forked from home-assistant/core
Compare commits
932 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21aa635ad8 | |||
| a70daabcea | |||
| baad8100f9 | |||
| 6ed46bf549 | |||
| 0c765a40ae | |||
| 4a2ed97e0d | |||
| 6af1a835e6 | |||
| d5bda3ac14 | |||
| 08719af794 | |||
| 6954614e62 | |||
| 3a56e3a823 | |||
| 8196a84538 | |||
| d61a9e8b72 | |||
| e729339538 | |||
| 53e130d9a8 | |||
| 55328d2c6f | |||
| 942db3fcbc | |||
| f18e4bab60 | |||
| dd52ec78c7 | |||
| ef4b6d7bdf | |||
| c9e1a03fe2 | |||
| d4ed0f9637 | |||
| cf6398a949 | |||
| ee28dd57c1 | |||
| c6f48056fd | |||
| d3b1ccb668 | |||
| 26042bdad7 | |||
| 4ae887ad34 | |||
| 1f5720199c | |||
| 0c1c1f7845 | |||
| 6a266ae3c0 | |||
| 4c854a06d9 | |||
| a035615016 | |||
| 4b68700763 | |||
| 8993ff0377 | |||
| a6a3745413 | |||
| 2e02945833 | |||
| 54abd80462 | |||
| 51addfc164 | |||
| 2ed35debdc | |||
| a7f554e6da | |||
| e9d25974b8 | |||
| 2ff1fc83bc | |||
| a967a1d1df | |||
| 12b2076351 | |||
| b9d81c3a7e | |||
| fa716d92ad | |||
| 8c3fc95fb8 | |||
| 7dfcccd43e | |||
| f8903e11e0 | |||
| 18340b2fd9 | |||
| f224ab6d67 | |||
| 0463007050 | |||
| 23cbd9075a | |||
| 25ce7f951a | |||
| a966714032 | |||
| 8f4ba564d4 | |||
| ef13e473cf | |||
| ae00c221e0 | |||
| f7ef973c68 | |||
| dbba2c4afe | |||
| 60eb426451 | |||
| 50fffe48f8 | |||
| d5c3d234ec | |||
| 40ecf22bac | |||
| d13c3e3917 | |||
| 00651a4055 | |||
| daebc34f4d | |||
| 77ee72cbb9 | |||
| d51487f82a | |||
| 364767ff22 | |||
| 19685ecff0 | |||
| 565a9fea6b | |||
| 41e5f05d99 | |||
| d3df6f26f9 | |||
| 52e9f76f94 | |||
| be34a2ddea | |||
| deb0cc4116 | |||
| 6a9b484f2d | |||
| 115bb39c10 | |||
| 34ef47db55 | |||
| 4513a46248 | |||
| 8e91e6e97e | |||
| cf36d0966d | |||
| f7d95588f8 | |||
| 718f8d8bf7 | |||
| 15aafc8db6 | |||
| a91fbec198 | |||
| 160571888c | |||
| e76ddb4b27 | |||
| db30c27455 | |||
| bc59387437 | |||
| c95f7a5ba6 | |||
| 0044fa9fb9 | |||
| b179301152 | |||
| b64b926e13 | |||
| 495e5cb1c0 | |||
| 2581a3a735 | |||
| 0d6aa89fd4 | |||
| 922d4c42a3 | |||
| 9d89e1ae00 | |||
| 8dc4824aac | |||
| e690d4b006 | |||
| 552485bb05 | |||
| c48527858d | |||
| 412ecacca3 | |||
| f93539ef4c | |||
| aea754df5d | |||
| 8ef123259e | |||
| ec9fc0052d | |||
| 5976f898da | |||
| 0653693dff | |||
| a28fd7d61b | |||
| b15f11f46a | |||
| b40d229369 | |||
| 7a2bc130b7 | |||
| 806fdc0095 | |||
| 2f4e992662 | |||
| f0e0b41f77 | |||
| 0dcd8b32ab | |||
| 71ce858378 | |||
| fe66d6295c | |||
| e5642a8648 | |||
| 4d7e3cde5a | |||
| eae828a15a | |||
| 805e73f78c | |||
| 70cc6295b5 | |||
| 4d433e18ac | |||
| 4ce7166afd | |||
| 83b1b3e92c | |||
| f2debf5c01 | |||
| 6da548b56a | |||
| 50f97b26eb | |||
| b612e16120 | |||
| 58f465f271 | |||
| 931cf4eaab | |||
| ca6b53c16d | |||
| efe467217a | |||
| 56b94d6809 | |||
| 4f5d6b8ba1 | |||
| 14a1bb423c | |||
| e4dc646237 | |||
| 6c2674734a | |||
| 0f313ec73c | |||
| 0fce9f39b3 | |||
| 01bd3ff138 | |||
| 8884e9691e | |||
| 55c9abc58d | |||
| 44a4507b51 | |||
| 115d34f55a | |||
| 7ab6c82ad2 | |||
| 26e031984b | |||
| 6399730d2f | |||
| f268227d64 | |||
| 8716aa011a | |||
| 2326e3ed94 | |||
| 52410ff0d7 | |||
| 26f73779cc | |||
| f74291ccb6 | |||
| e35e584b60 | |||
| 65bce33a63 | |||
| bfefe82605 | |||
| a0a359e2ef | |||
| d8387744ec | |||
| 01e03a223b | |||
| aeba3a703f | |||
| 8db0bd3c0e | |||
| d4ebcf2ba5 | |||
| fabf5204be | |||
| 83f1116432 | |||
| 23e1c663d4 | |||
| 754ff7e3cb | |||
| 8bc8081e81 | |||
| 917254e956 | |||
| 57aaf55678 | |||
| f0de6dc21a | |||
| 5c1f55ffa4 | |||
| b1f4ccfd6b | |||
| 5d3d6fa1cd | |||
| 0ea5f25594 | |||
| a7d56d1c3f | |||
| ac576a2bc6 | |||
| 3c80e05100 | |||
| 8aff51042b | |||
| e078f4ce14 | |||
| d7e121f3e8 | |||
| aca00667df | |||
| 64393b462d | |||
| 7452998081 | |||
| e62c9d338e | |||
| e73ca9bd18 | |||
| 0363c22dd8 | |||
| 915afedcfc | |||
| 0b53f73fe2 | |||
| 7ece35cd6f | |||
| 972db29c88 | |||
| 6e7bc65e2e | |||
| a94514b00d | |||
| 4c40d1767a | |||
| 7fc0717ab1 | |||
| 750a1b84ad | |||
| fed5f5e3b9 | |||
| 60bb3121b6 | |||
| b43d377b53 | |||
| a5c6a65161 | |||
| 2fe8c78811 | |||
| 1cc850877f | |||
| cce906f968 | |||
| 70f10338cc | |||
| 98d0c84468 | |||
| 1a47fcc4e3 | |||
| b634bd26d0 | |||
| c1df49e9fc | |||
| 442d850fc9 | |||
| 237efcf6b1 | |||
| bdef131294 | |||
| 71ab24f350 | |||
| 655dc890e4 | |||
| a6ccb1821e | |||
| f0a4a89d21 | |||
| 63610eadc9 | |||
| ea8f624f28 | |||
| 83156fb9ec | |||
| 974376a8de | |||
| 677abcd484 | |||
| f77e93ceeb | |||
| 351ef0ab44 | |||
| 92253f5192 | |||
| d8d34fdd3b | |||
| 39aaa383b3 | |||
| a5d405700c | |||
| 2478ec887a | |||
| e34c985534 | |||
| ac053388b4 | |||
| 26d310fc8a | |||
| aab4b5ec06 | |||
| 783cc1eacd | |||
| a653da137c | |||
| b7a758bd0c | |||
| 9831ff0487 | |||
| d494b3539d | |||
| 1aa7c87151 | |||
| 26e9590927 | |||
| 34de74d869 | |||
| 56b66d5124 | |||
| c7c789f618 | |||
| 518c99c8b7 | |||
| 7698c179ac | |||
| f9fde7f7a1 | |||
| 097fae0348 | |||
| d4864f5750 | |||
| aabc8cd2d5 | |||
| 9059ee6604 | |||
| b9ffd74db5 | |||
| 20ddd092f6 | |||
| 47340802b3 | |||
| df56953c98 | |||
| 542f637ac4 | |||
| 9e2a29dc37 | |||
| f3ad4ca0cc | |||
| 4c4bd740f3 | |||
| 6f36419c6f | |||
| fc4bb40a63 | |||
| a84e86ff13 | |||
| bb6f97c4d3 | |||
| 93e9a67d7d | |||
| 5c717cbb1d | |||
| a54854d129 | |||
| be19c676fa | |||
| ddd31951bc | |||
| b05c1b516e | |||
| b060c025ce | |||
| 00f7548fa0 | |||
| d76163e5be | |||
| ea189f930a | |||
| 713d294627 | |||
| 88e42a540e | |||
| ec52763706 | |||
| 80a57f5118 | |||
| 75c029c56b | |||
| a57d7717a8 | |||
| 53d4c0ce2d | |||
| 7af67d34cf | |||
| a4f6c3336f | |||
| 9b710cad5d | |||
| f6526de7b6 | |||
| 8b64cd7e7d | |||
| 43b5dcff76 | |||
| bf7c2753d5 | |||
| 6b6e26c96d | |||
| 5c19368ce3 | |||
| 3ce8109e5e | |||
| f31b9eae61 | |||
| 312a9e5df2 | |||
| f97cce6f57 | |||
| 9ffdc2594f | |||
| 0a5948dc8b | |||
| 811feb69ba | |||
| 0a5fdb2e68 | |||
| 48bada5a18 | |||
| 476d04e2fb | |||
| 39bc127dd6 | |||
| be0819b456 | |||
| 0830100df1 | |||
| 539ef31046 | |||
| 6947912fa9 | |||
| 02ba3c6089 | |||
| f8d62866e5 | |||
| 3046adf3b2 | |||
| 22307066bb | |||
| 8c936b320e | |||
| 9c4d6afb3c | |||
| 0442ed54e8 | |||
| 3224fc8710 | |||
| 6f40759ed0 | |||
| 07ba24ac5a | |||
| c0c85a6641 | |||
| 162fab91b2 | |||
| 6e80d27770 | |||
| 2b58a266cc | |||
| bd17da0524 | |||
| 7524daad86 | |||
| aaadd42539 | |||
| b6763c7245 | |||
| eb98ac9415 | |||
| bad6b2f7f5 | |||
| 4160a5ee3b | |||
| f5dd71d1e0 | |||
| c4195c547c | |||
| f64aa0f8df | |||
| e880f1c8f9 | |||
| 5b0e00a74b | |||
| 16832bc35b | |||
| 45046941c6 | |||
| 9b00e0cb7a | |||
| ecf4a7813a | |||
| 5249c89c3f | |||
| 797b68b42d | |||
| 55a77b2ba2 | |||
| 327bf24940 | |||
| e0a232aa36 | |||
| a793fd4134 | |||
| bce4c5eb11 | |||
| fce7f0873e | |||
| 8814c53504 | |||
| 6d99a7a730 | |||
| 175f207d28 | |||
| 8341ae12d3 | |||
| 70eb519f76 | |||
| 94f06f86cf | |||
| 15a7fe219d | |||
| 41bf1eb610 | |||
| 8418d4ade2 | |||
| 1609d069bb | |||
| 28b4b5407b | |||
| 0438c9308c | |||
| c668dcb1ac | |||
| 8c5efafdd8 | |||
| 0656407561 | |||
| 69ff7a968a | |||
| 4eb656d5d9 | |||
| ecd827722b | |||
| c5544550b4 | |||
| 19054e1ffe | |||
| 0619069ae6 | |||
| 53d5a59257 | |||
| ddfe94187e | |||
| 0d842a8f01 | |||
| 30c25d4448 | |||
| 0a426b5686 | |||
| 98cf34c7c3 | |||
| 692f611109 | |||
| bbc75b5c00 | |||
| 441e99b439 | |||
| 96a9af8cc4 | |||
| 2c348dd2d7 | |||
| 4b2ff0a0ba | |||
| 2b51896d7a | |||
| 0364922d80 | |||
| 2a51bb5bba | |||
| bac55b78fe | |||
| aaa62dadec | |||
| 31623368c8 | |||
| dba2998e8c | |||
| fe1311ba34 | |||
| 14aa9c91eb | |||
| 9bb9f0e070 | |||
| 8d87f4148b | |||
| c869b78ac1 | |||
| f023ec24d7 | |||
| f9de8fb49a | |||
| d661a76462 | |||
| 20d96ef4ef | |||
| 17efafb2ea | |||
| 86d24bec75 | |||
| 5f86388f1c | |||
| 37f263e2ac | |||
| e638e5bb42 | |||
| ee616ed992 | |||
| 5ccc3c17d9 | |||
| d899d15a1e | |||
| d2a9f7904a | |||
| e9eb76c7db | |||
| 7472fb2049 | |||
| 1f997fcd58 | |||
| a180c3f813 | |||
| 32212651fe | |||
| 41b25a765c | |||
| 990d474d02 | |||
| 673519f6bf | |||
| 0fc89780e9 | |||
| 4e8db7173a | |||
| f1556ead6d | |||
| 2b019b0911 | |||
| ec28f7eef2 | |||
| 4a449902a5 | |||
| 2b7cdb70a8 | |||
| 371aa03bca | |||
| eff59e8b00 | |||
| 459bc55e32 | |||
| 73260c5b88 | |||
| f1a88f0563 | |||
| ddb0a6092f | |||
| a4a6bf8a85 | |||
| bcb3c426f4 | |||
| 64fd496932 | |||
| 292c05ab9f | |||
| a900c02c10 | |||
| 0cfbff3ff9 | |||
| ee892beceb | |||
| fe713b943f | |||
| 2079956350 | |||
| c424f99aab | |||
| 8a611eb640 | |||
| 6e7ce89c64 | |||
| cb8c0cb123 | |||
| a9ed4fa405 | |||
| ed9b271fd0 | |||
| 1b46190a0c | |||
| ec21c5f4a9 | |||
| 97f28878bb | |||
| b11db0b1d7 | |||
| bd18bc9f3a | |||
| c785983cce | |||
| 8c3c2ad8e3 | |||
| ac1251c52b | |||
| dec7877671 | |||
| 443147e132 | |||
| c59540cfc7 | |||
| aa39e582c3 | |||
| 03df48af9c | |||
| 948a942a0d | |||
| d5a8f1af1d | |||
| ff1b39cda6 | |||
| 970a7f9662 | |||
| 78909b5227 | |||
| e990ef249d | |||
| c27ad3078a | |||
| 89281a273c | |||
| e3a7a253ea | |||
| 5709640453 | |||
| 2a8121bdcd | |||
| 1fd3faf766 | |||
| 3af4b2639b | |||
| f79de2a5bc | |||
| 113288cb1f | |||
| 88dbc6373f | |||
| 1b0e014783 | |||
| a47532c69b | |||
| dd9bfe7aa0 | |||
| 556dcf6abb | |||
| 011817b122 | |||
| cbbbc3c4f0 | |||
| 9d2861afe3 | |||
| 065e858a03 | |||
| 80fd330479 | |||
| a8cbb949fa | |||
| 98ecf2888c | |||
| 675426dc25 | |||
| a1aca20818 | |||
| 5cc54618c5 | |||
| 4d2432cffb | |||
| 8f344252c4 | |||
| 232943c93d | |||
| cbe4b2dc1d | |||
| bb6c2093a2 | |||
| a17d2d7c71 | |||
| ee7202d10a | |||
| e3815c6c2e | |||
| 5cba7932f3 | |||
| 413430bdba | |||
| 81462d8655 | |||
| 8ee4b49aa9 | |||
| 19d7cb4439 | |||
| 21ebf4f3e6 | |||
| 980fcef36f | |||
| e7fd24eade | |||
| 9ecb75dc70 | |||
| 7f3adce675 | |||
| 27764e9985 | |||
| 9f1e503784 | |||
| 22e6ddf8df | |||
| c514f72c70 | |||
| 84140a547b | |||
| 0b23ce658e | |||
| 32b8be5a6e | |||
| 69d6d5ffce | |||
| 5429a67877 | |||
| 7195b8222b | |||
| a764c79b6f | |||
| ec337101dd | |||
| efafe82799 | |||
| f9e6e616f4 | |||
| 37d75e8a03 | |||
| 42f586c585 | |||
| d705b35ea1 | |||
| 0684f8bddf | |||
| 3aed58f825 | |||
| 86247c93fc | |||
| 53ea24ec15 | |||
| 2f3a11f930 | |||
| 0d1412ea17 | |||
| 1ca9deb520 | |||
| 789f21c427 | |||
| 9da3fa5d75 | |||
| 93083513b4 | |||
| f0649855f9 | |||
| c6888e4faf | |||
| b1dbdec2ea | |||
| 6895081595 | |||
| 823c3735ce | |||
| 68131a5c00 | |||
| be0f767c34 | |||
| 450652a501 | |||
| 8523f569c0 | |||
| 5f289434d3 | |||
| 3df6dfecab | |||
| d6eda65302 | |||
| 7a5bc2784a | |||
| 00878467cc | |||
| 899d8164b0 | |||
| 823fd60991 | |||
| a6bb0eadca | |||
| eb70354ee7 | |||
| bd53185bed | |||
| 34d54511e8 | |||
| 8d4aac618d | |||
| 4fa9871080 | |||
| b088ce601c | |||
| bcfedeb797 | |||
| 9093819671 | |||
| cac3e1acfa | |||
| eba9b61011 | |||
| 0533a9c714 | |||
| 12b1f87b35 | |||
| 8b6d0ca13f | |||
| dd7dea9a3f | |||
| b99a22cd4d | |||
| 2634949999 | |||
| e671ad41ec | |||
| 9ee0d8fefe | |||
| e6a29b6a2a | |||
| df928c80b8 | |||
| 05abf1405d | |||
| 753285eae7 | |||
| 67b7144703 | |||
| d50b700dc7 | |||
| 4475cf24c8 | |||
| 3001df99cb | |||
| 364edbfd8a | |||
| 755835ee2e | |||
| a4e4ffef0a | |||
| 655399eb7b | |||
| 77b60c712e | |||
| a4dae0c1e1 | |||
| 96db04213b | |||
| cc6a0d2f8d | |||
| 99ef2ae54d | |||
| 0dd128af77 | |||
| 1b3530a3f8 | |||
| 8565821394 | |||
| 523998f8a1 | |||
| c2b89725be | |||
| 22961b30d2 | |||
| aa6cb84b27 | |||
| 4e1e7a4a71 | |||
| 5a2bcd2763 | |||
| f8ebc31576 | |||
| cce0ca5688 | |||
| df9a899bbd | |||
| 37cf295e20 | |||
| 04816fe26d | |||
| eb48e75fc5 | |||
| 9d5431fba1 | |||
| a4f2c5583d | |||
| a37c3af2b4 | |||
| d39b861110 | |||
| 715ce3185b | |||
| c81a319346 | |||
| f5a543b220 | |||
| 58da58c008 | |||
| 6348bf70ac | |||
| 0700108278 | |||
| d8b85b2067 | |||
| b7e8348c30 | |||
| 38d42de2c0 | |||
| b3181a0ab2 | |||
| 10317fba17 | |||
| 7aa454231f | |||
| 19c54b8cbf | |||
| 195ee2a188 | |||
| 0dc8fb497b | |||
| b10fc89a6b | |||
| 0749e045bb | |||
| 19dcb19d07 | |||
| 501e7c84be | |||
| a54b9502ef | |||
| 617e8544c0 | |||
| edddeaf5ab | |||
| a756308e79 | |||
| cd51d994b1 | |||
| 4eba2ccebc | |||
| f5cd321185 | |||
| bbd9c6eb5b | |||
| ce6921d73c | |||
| 9db13a3e74 | |||
| 1e4233fe20 | |||
| 76ce0f6ea7 | |||
| 798f487ea4 | |||
| 3c0a34dd01 | |||
| fbf812a845 | |||
| 33047d7260 | |||
| e3405d226a | |||
| 3008ff03b2 | |||
| 8592d94a3c | |||
| b36e86d95c | |||
| f61a1ecae7 | |||
| 80c074ca82 | |||
| ff91ff4cd2 | |||
| 93c2a7dd70 | |||
| da3ee9ed4b | |||
| 2ef607651d | |||
| 88ca83a30b | |||
| 7111fc47c4 | |||
| e0f640c0f8 | |||
| 7caa985a59 | |||
| 25b39b36e7 | |||
| 418d6a6a41 | |||
| a234f2ab31 | |||
| 7461af68b9 | |||
| 2171922265 | |||
| 4310a7d814 | |||
| ae9e3c237a | |||
| b4d4fe4ef8 | |||
| 70338da50e | |||
| 173b87e675 | |||
| 91cd6951f3 | |||
| 4684ea2d14 | |||
| 0c2772e0be | |||
| 8319f232b8 | |||
| d8a81a54d8 | |||
| 8af0cb9e65 | |||
| f883fa9eef | |||
| 5b705dba36 | |||
| 1592408a4b | |||
| 89b7be52af | |||
| 8f85472df3 | |||
| 6aa771e5e8 | |||
| 7193e82963 | |||
| 02db4dbe5e | |||
| 7dbe8070f7 | |||
| 363320eedb | |||
| 2e5c1236f9 | |||
| 348bdca647 | |||
| 4f33679255 | |||
| cabb9c0ea4 | |||
| d4a2b36638 | |||
| bfd799dc04 | |||
| acdddabe1f | |||
| d5b6dc4f26 | |||
| 69aba2a6a1 | |||
| cdaba62d2c | |||
| b3b9fb0a7c | |||
| cb1e0666c8 | |||
| 6b4f2e6f8f | |||
| aef4a69cd0 | |||
| 02eba22068 | |||
| 7dbd0e5274 | |||
| e631671832 | |||
| 245eec7041 | |||
| 493309daa7 | |||
| af68802c17 | |||
| 576cece7a9 | |||
| 3b9859940f | |||
| 27e29b714c | |||
| c68e87c40e | |||
| 80af2f4279 | |||
| f8ec85686a | |||
| 33fb080c1e | |||
| 9284f7b147 | |||
| bcf97cb308 | |||
| 04a052a37d | |||
| befcafbc49 | |||
| 02b7356596 | |||
| 46159c3f18 | |||
| a28593f133 | |||
| 889aced3b6 | |||
| a315fd059a | |||
| ba9ef004c8 | |||
| 22f745b17c | |||
| 05cf223146 | |||
| d4aadd8af0 | |||
| 4045eee2e5 | |||
| 83a51f7f30 | |||
| 29110fe157 | |||
| e87b7e24b4 | |||
| d9056c01a6 | |||
| a724bc21b6 | |||
| ef00178339 | |||
| b8770c3958 | |||
| f0c0cfcac0 | |||
| 36b37b6db3 | |||
| 3bc58f9750 | |||
| 343054494c | |||
| 93c086d830 | |||
| 9e41a37284 | |||
| ff229dd599 | |||
| cc4b2fbcfa | |||
| 5d1a193eca | |||
| 71c6f99d31 | |||
| bd60a58765 | |||
| 08a0377dcb | |||
| 4d98a7e156 | |||
| 3e38dc0fd9 | |||
| afc0a1f376 | |||
| 1849eae0ff | |||
| f9225bad5f | |||
| 88a08fdf57 | |||
| d277e0fb03 | |||
| 13b001cd9b | |||
| dd21bf73fc | |||
| 368cac7e5d | |||
| 4c48ad9108 | |||
| 92b0453749 | |||
| 8ab801a7b4 | |||
| f92c7b1aea | |||
| 0d9fbf864f | |||
| 275f9c8a28 | |||
| 84f3b1514f | |||
| 802f5613c4 | |||
| 18c03e2f8d | |||
| 9b3346bc80 | |||
| 76f21452ee | |||
| 433775cf4b | |||
| 46f05ca279 | |||
| 3d9d104482 | |||
| 1d1b5ab345 | |||
| 1c01ff401f | |||
| 3bd9be2f6d | |||
| daa9c8d856 | |||
| 1aa30ea87b | |||
| 8be40cbb00 | |||
| 46ce4e92f6 | |||
| 39f11bb46d | |||
| 3b0fe9adde | |||
| 707778229b | |||
| a474534c08 | |||
| 65ad99d51c | |||
| 4052a0db89 | |||
| b546fc5067 | |||
| 5dcc760755 | |||
| 331726ec2f | |||
| 27ecd43da3 | |||
| d62a78ae61 | |||
| fa7873dc6d | |||
| de5a22953d | |||
| cbc68e45cd | |||
| c4235edc41 | |||
| ed53bb1d91 | |||
| a668300c2e | |||
| 722aa0895e | |||
| 7e9f8de7e0 | |||
| 8faec3da8d | |||
| 1060630bbd | |||
| 25273c694a | |||
| 071fcee9a9 | |||
| fb06acf39d | |||
| 948f191f16 | |||
| 2c0d9105ac | |||
| 10df9f3542 | |||
| 6cf799459b | |||
| 47e2d1caa5 | |||
| 69d8f94e3b | |||
| 4b7803ed03 | |||
| ff6015ff89 | |||
| fbd144de46 | |||
| 5549a925b8 | |||
| be04d7b92e | |||
| f37c541a50 | |||
| 6823b14d4c | |||
| 94e0db8ec4 | |||
| ebc2a0103e | |||
| ea7f3c8bb3 | |||
| 32df2f7d8b | |||
| b43c80ca21 | |||
| fa201b6c2b | |||
| 76ce33dc24 | |||
| 8b436c43f7 | |||
| fd66120d6d | |||
| 43b8353566 | |||
| 4aed0b6ccf | |||
| 3647ada143 | |||
| 2dddd31d97 | |||
| 923158cfba | |||
| 291a2d6258 | |||
| 43288d3e1f | |||
| d41fa66bca | |||
| f1ba98927c | |||
| f91cc21bbd | |||
| 13cc671844 | |||
| 979797136a | |||
| 778fa2e3fe | |||
| adaebdeea8 | |||
| 910cb5865a | |||
| baf0d9b2d9 | |||
| c1bce68549 | |||
| bde4c0e46f | |||
| a275e7aa67 | |||
| d96e416d26 | |||
| efc3894303 | |||
| 06b47ee2f5 | |||
| 08ca43221f | |||
| 8641740ed8 | |||
| 6a93f5b7ad | |||
| 19873e6547 | |||
| 2fcd77098d | |||
| d1965eef8b | |||
| 16351ef3c2 | |||
| e2f257cb63 | |||
| ea8702b0df | |||
| 470aa7e871 | |||
| 61a7ce173c | |||
| b0c52220bc | |||
| 714564eaa6 | |||
| 1f37c215f6 | |||
| 46d0523f98 | |||
| eb458fb1d5 | |||
| 10fa63775d | |||
| d0ada6c6e2 | |||
| 76bb036968 | |||
| d8b64be41c | |||
| b3e0b7b86e | |||
| e097e4c1c2 | |||
| 34f0fecef8 | |||
| f53a10d39a | |||
| 5b993129d6 | |||
| 865656d436 | |||
| fb25c6c115 | |||
| c963cf8743 | |||
| ddb28db21a | |||
| bfc98b444f | |||
| f9a0f44137 | |||
| 93750d71ce | |||
| 06e4003640 | |||
| 97ff5e2085 | |||
| 8a2c07ce19 | |||
| 9f7398e0df | |||
| 7df84dadad | |||
| 2a1e943b18 | |||
| e6e72bfa82 | |||
| ed19fdd462 | |||
| 2cc87cb7ab | |||
| 7ac72ebf38 | |||
| 98c8782c2b | |||
| 7bd7d644a0 | |||
| 3f2fad1a27 | |||
| 819fd811af | |||
| e2dac31471 | |||
| 7e70252de5 | |||
| cc857abfd2 | |||
| adab367f0e | |||
| 259eeb3169 | |||
| efb1fb9978 | |||
| 5693f9ff9b | |||
| 9ba504cd78 | |||
| 176fd39e0b | |||
| cd0ae66d58 | |||
| 65d14909ee | |||
| 5393a16c44 | |||
| cbd65efe52 | |||
| ef10773202 | |||
| dfc2556669 | |||
| 14aa19b814 | |||
| c3972b22fd | |||
| ae1d2926cf | |||
| 089dfad78a | |||
| f6bb5c77a0 | |||
| eb9d242ade | |||
| 219868b308 | |||
| 67dd861d8c | |||
| f2765ba320 | |||
| aefd3df914 | |||
| 3658eeb8d1 | |||
| 080cb6b6e9 | |||
| 20796303da | |||
| dff6151ff4 | |||
| 6f24f4e302 | |||
| 175febe635 | |||
| aa907f4d10 | |||
| 3d09478aea | |||
| 05df9b4b8b | |||
| 1865a28083 | |||
| fbcf21412d | |||
| b3e84c6ee8 | |||
| c3316df31d | |||
| f942cb03a4 | |||
| d3ac72d013 | |||
| d59ea5329e | |||
| d4fa625a7f | |||
| a89057ece5 | |||
| 2d5176eee9 | |||
| 0a07ff4d23 | |||
| 96303a1d80 | |||
| 03d3bbfba1 | |||
| 56246056ce | |||
| 6d4a47a53d | |||
| e6d710c203 | |||
| b45c985d58 | |||
| 3d7bfa8357 | |||
| b81c2806bb | |||
| 06a30c882f | |||
| 9315f3bdd9 |
+31
-20
@@ -36,6 +36,8 @@ omit =
|
||||
homeassistant/components/agent_dvr/helpers.py
|
||||
homeassistant/components/airnow/__init__.py
|
||||
homeassistant/components/airnow/sensor.py
|
||||
homeassistant/components/airthings/__init__.py
|
||||
homeassistant/components/airthings/sensor.py
|
||||
homeassistant/components/airtouch4/__init__.py
|
||||
homeassistant/components/airtouch4/climate.py
|
||||
homeassistant/components/airtouch4/const.py
|
||||
@@ -49,6 +51,7 @@ omit =
|
||||
homeassistant/components/alarmdecoder/sensor.py
|
||||
homeassistant/components/alpha_vantage/sensor.py
|
||||
homeassistant/components/amazon_polly/*
|
||||
homeassistant/components/amberelectric/__init__.py
|
||||
homeassistant/components/ambiclimate/climate.py
|
||||
homeassistant/components/ambient_station/*
|
||||
homeassistant/components/amcrest/*
|
||||
@@ -171,6 +174,13 @@ omit =
|
||||
homeassistant/components/coolmaster/const.py
|
||||
homeassistant/components/cppm_tracker/device_tracker.py
|
||||
homeassistant/components/cpuspeed/sensor.py
|
||||
homeassistant/components/crownstone/__init__.py
|
||||
homeassistant/components/crownstone/const.py
|
||||
homeassistant/components/crownstone/listeners.py
|
||||
homeassistant/components/crownstone/helpers.py
|
||||
homeassistant/components/crownstone/devices.py
|
||||
homeassistant/components/crownstone/entry_manager.py
|
||||
homeassistant/components/crownstone/light.py
|
||||
homeassistant/components/cups/sensor.py
|
||||
homeassistant/components/currencylayer/sensor.py
|
||||
homeassistant/components/daikin/*
|
||||
@@ -203,7 +213,6 @@ omit =
|
||||
homeassistant/components/dlib_face_detect/image_processing.py
|
||||
homeassistant/components/dlib_face_identify/image_processing.py
|
||||
homeassistant/components/dlink/switch.py
|
||||
homeassistant/components/dlna_dmr/media_player.py
|
||||
homeassistant/components/dnsip/sensor.py
|
||||
homeassistant/components/dominos/*
|
||||
homeassistant/components/doods/*
|
||||
@@ -368,7 +377,6 @@ omit =
|
||||
homeassistant/components/garages_amsterdam/sensor.py
|
||||
homeassistant/components/gc100/*
|
||||
homeassistant/components/geniushub/*
|
||||
homeassistant/components/generic_hygrostat/*
|
||||
homeassistant/components/github/sensor.py
|
||||
homeassistant/components/gitlab_ci/sensor.py
|
||||
homeassistant/components/gitter/sensor.py
|
||||
@@ -687,7 +695,6 @@ omit =
|
||||
homeassistant/components/nad/media_player.py
|
||||
homeassistant/components/nanoleaf/__init__.py
|
||||
homeassistant/components/nanoleaf/light.py
|
||||
homeassistant/components/nanoleaf/util.py
|
||||
homeassistant/components/neato/__init__.py
|
||||
homeassistant/components/neato/api.py
|
||||
homeassistant/components/neato/camera.py
|
||||
@@ -699,7 +706,10 @@ omit =
|
||||
homeassistant/components/nello/lock.py
|
||||
homeassistant/components/nest/legacy/*
|
||||
homeassistant/components/netdata/sensor.py
|
||||
homeassistant/components/netgear/__init__.py
|
||||
homeassistant/components/netgear/device_tracker.py
|
||||
homeassistant/components/netgear/router.py
|
||||
homeassistant/components/netgear/sensor.py
|
||||
homeassistant/components/netgear_lte/*
|
||||
homeassistant/components/netio/switch.py
|
||||
homeassistant/components/neurio_energy/sensor.py
|
||||
@@ -754,6 +764,7 @@ omit =
|
||||
homeassistant/components/opencv/*
|
||||
homeassistant/components/openevse/sensor.py
|
||||
homeassistant/components/openexchangerates/sensor.py
|
||||
homeassistant/components/opengarage/__init__.py
|
||||
homeassistant/components/opengarage/cover.py
|
||||
homeassistant/components/openhome/__init__.py
|
||||
homeassistant/components/openhome/media_player.py
|
||||
@@ -864,9 +875,6 @@ omit =
|
||||
homeassistant/components/rest/switch.py
|
||||
homeassistant/components/ring/camera.py
|
||||
homeassistant/components/ripple/sensor.py
|
||||
homeassistant/components/rituals_perfume_genie/binary_sensor.py
|
||||
homeassistant/components/rituals_perfume_genie/number.py
|
||||
homeassistant/components/rituals_perfume_genie/select.py
|
||||
homeassistant/components/rocketchat/notify.py
|
||||
homeassistant/components/roomba/__init__.py
|
||||
homeassistant/components/roomba/binary_sensor.py
|
||||
@@ -990,7 +998,6 @@ omit =
|
||||
homeassistant/components/squeezebox/__init__.py
|
||||
homeassistant/components/squeezebox/browse_media.py
|
||||
homeassistant/components/squeezebox/media_player.py
|
||||
homeassistant/components/ssdp/util.py
|
||||
homeassistant/components/starline/*
|
||||
homeassistant/components/starlingbank/sensor.py
|
||||
homeassistant/components/steam_online/sensor.py
|
||||
@@ -1001,12 +1008,20 @@ omit =
|
||||
homeassistant/components/suez_water/*
|
||||
homeassistant/components/supervisord/sensor.py
|
||||
homeassistant/components/surepetcare/__init__.py
|
||||
homeassistant/components/surepetcare/entity.py
|
||||
homeassistant/components/surepetcare/binary_sensor.py
|
||||
homeassistant/components/surepetcare/sensor.py
|
||||
homeassistant/components/swiss_hydrological_data/sensor.py
|
||||
homeassistant/components/swiss_public_transport/sensor.py
|
||||
homeassistant/components/swisscom/device_tracker.py
|
||||
homeassistant/components/switchbot/switch.py
|
||||
homeassistant/components/switchbot/binary_sensor.py
|
||||
homeassistant/components/switchbot/__init__.py
|
||||
homeassistant/components/switchbot/const.py
|
||||
homeassistant/components/switchbot/entity.py
|
||||
homeassistant/components/switchbot/cover.py
|
||||
homeassistant/components/switchbot/sensor.py
|
||||
homeassistant/components/switchbot/coordinator.py
|
||||
homeassistant/components/switchmate/switch.py
|
||||
homeassistant/components/syncthing/__init__.py
|
||||
homeassistant/components/syncthing/sensor.py
|
||||
@@ -1032,6 +1047,8 @@ omit =
|
||||
homeassistant/components/tank_utility/sensor.py
|
||||
homeassistant/components/tankerkoenig/*
|
||||
homeassistant/components/tapsaff/binary_sensor.py
|
||||
homeassistant/components/tautulli/const.py
|
||||
homeassistant/components/tautulli/coordinator.py
|
||||
homeassistant/components/tautulli/sensor.py
|
||||
homeassistant/components/ted5000/sensor.py
|
||||
homeassistant/components/telegram/notify.py
|
||||
@@ -1047,14 +1064,6 @@ omit =
|
||||
homeassistant/components/telnet/switch.py
|
||||
homeassistant/components/temper/sensor.py
|
||||
homeassistant/components/tensorflow/image_processing.py
|
||||
homeassistant/components/tesla/__init__.py
|
||||
homeassistant/components/tesla/binary_sensor.py
|
||||
homeassistant/components/tesla/climate.py
|
||||
homeassistant/components/tesla/const.py
|
||||
homeassistant/components/tesla/device_tracker.py
|
||||
homeassistant/components/tesla/lock.py
|
||||
homeassistant/components/tesla/sensor.py
|
||||
homeassistant/components/tesla/switch.py
|
||||
homeassistant/components/tfiac/climate.py
|
||||
homeassistant/components/thermoworks_smoke/sensor.py
|
||||
homeassistant/components/thethingsnetwork/*
|
||||
@@ -1088,16 +1097,15 @@ omit =
|
||||
homeassistant/components/totalconnect/binary_sensor.py
|
||||
homeassistant/components/totalconnect/const.py
|
||||
homeassistant/components/touchline/climate.py
|
||||
homeassistant/components/tplink/common.py
|
||||
homeassistant/components/tplink/switch.py
|
||||
homeassistant/components/tplink_lte/*
|
||||
homeassistant/components/traccar/device_tracker.py
|
||||
homeassistant/components/traccar/const.py
|
||||
homeassistant/components/trackr/device_tracker.py
|
||||
homeassistant/components/tractive/__init__.py
|
||||
homeassistant/components/tractive/binary_sensor.py
|
||||
homeassistant/components/tractive/device_tracker.py
|
||||
homeassistant/components/tractive/entity.py
|
||||
homeassistant/components/tractive/sensor.py
|
||||
homeassistant/components/tractive/switch.py
|
||||
homeassistant/components/tradfri/*
|
||||
homeassistant/components/trafikverket_train/sensor.py
|
||||
homeassistant/components/trafikverket_weatherstation/sensor.py
|
||||
@@ -1107,9 +1115,9 @@ omit =
|
||||
homeassistant/components/transmission/errors.py
|
||||
homeassistant/components/travisci/sensor.py
|
||||
homeassistant/components/tuya/__init__.py
|
||||
homeassistant/components/tuya/base.py
|
||||
homeassistant/components/tuya/climate.py
|
||||
homeassistant/components/tuya/const.py
|
||||
homeassistant/components/tuya/cover.py
|
||||
homeassistant/components/tuya/fan.py
|
||||
homeassistant/components/tuya/light.py
|
||||
homeassistant/components/tuya/scene.py
|
||||
@@ -1177,6 +1185,8 @@ omit =
|
||||
homeassistant/components/waterfurnace/*
|
||||
homeassistant/components/watson_iot/*
|
||||
homeassistant/components/watson_tts/tts.py
|
||||
homeassistant/components/watttime/__init__.py
|
||||
homeassistant/components/watttime/sensor.py
|
||||
homeassistant/components/waze_travel_time/__init__.py
|
||||
homeassistant/components/waze_travel_time/helpers.py
|
||||
homeassistant/components/waze_travel_time/sensor.py
|
||||
@@ -1283,5 +1293,6 @@ exclude_lines =
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
|
||||
# TYPE_CHECKING block is never executed during pytest run
|
||||
# TYPE_CHECKING and @overload blocks are never executed during pytest run
|
||||
if TYPE_CHECKING:
|
||||
@overload
|
||||
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2021.07.0
|
||||
uses: home-assistant/builder@2021.09.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@@ -186,7 +186,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2021.07.0
|
||||
uses: home-assistant/builder@2021.09.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
pull_request: ~
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 2
|
||||
CACHE_VERSION: 3
|
||||
DEFAULT_PYTHON: 3.8
|
||||
PRE_COMMIT_CACHE: ~/.cache/pre-commit
|
||||
SQLALCHEMY_WARN_20: 1
|
||||
@@ -740,4 +740,4 @@ jobs:
|
||||
coverage report --fail-under=94
|
||||
coverage xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2.0.3
|
||||
uses: codecov/codecov-action@v2.1.0
|
||||
|
||||
@@ -9,12 +9,12 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v2.1.2
|
||||
- uses: dessant/lock-threads@v3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-lock-inactive-days: "30"
|
||||
issue-exclude-created-before: "2020-10-01T00:00:00Z"
|
||||
issue-inactive-days: "30"
|
||||
exclude-issue-created-before: "2020-10-01T00:00:00Z"
|
||||
issue-lock-reason: ""
|
||||
pr-lock-inactive-days: "1"
|
||||
pr-exclude-created-before: "2020-11-01T00:00:00Z"
|
||||
pr-inactive-days: "1"
|
||||
exclude-pr-created-before: "2020-11-01T00:00:00Z"
|
||||
pr-lock-reason: ""
|
||||
|
||||
@@ -65,7 +65,6 @@ jobs:
|
||||
matrix:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
tag:
|
||||
- "3.9-alpine3.13"
|
||||
- "3.9-alpine3.14"
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
@@ -90,7 +89,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
wheels-user: wheels
|
||||
env-file: true
|
||||
apk: "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev"
|
||||
apk: "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;cargo"
|
||||
pip: "Cython;numpy"
|
||||
skip-binary: aiohttp
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
@@ -106,7 +105,6 @@ jobs:
|
||||
matrix:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
tag:
|
||||
- "3.9-alpine3.13"
|
||||
- "3.9-alpine3.14"
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
@@ -160,7 +158,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
wheels-user: wheels
|
||||
env-file: true
|
||||
apk: "build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev"
|
||||
apk: "build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;cargo"
|
||||
pip: "Cython;numpy;scikit-build"
|
||||
skip-binary: aiohttp
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
config/*
|
||||
/config
|
||||
config2/*
|
||||
|
||||
tests/testing_config/deps
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
repos:
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.23.3
|
||||
rev: v2.27.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py38-plus]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 21.7b0
|
||||
rev: 21.9b0
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
|
||||
@@ -27,9 +27,11 @@ homeassistant.components.calendar.*
|
||||
homeassistant.components.camera.*
|
||||
homeassistant.components.canary.*
|
||||
homeassistant.components.cover.*
|
||||
homeassistant.components.crownstone.*
|
||||
homeassistant.components.device_automation.*
|
||||
homeassistant.components.device_tracker.*
|
||||
homeassistant.components.devolo_home_control.*
|
||||
homeassistant.components.dlna_dmr.*
|
||||
homeassistant.components.dnsip.*
|
||||
homeassistant.components.dsmr.*
|
||||
homeassistant.components.dunehd.*
|
||||
@@ -54,6 +56,7 @@ homeassistant.components.huawei_lte.*
|
||||
homeassistant.components.hyperion.*
|
||||
homeassistant.components.image_processing.*
|
||||
homeassistant.components.integration.*
|
||||
homeassistant.components.iqvia.*
|
||||
homeassistant.components.knx.*
|
||||
homeassistant.components.kraken.*
|
||||
homeassistant.components.lcn.*
|
||||
@@ -62,6 +65,7 @@ homeassistant.components.local_ip.*
|
||||
homeassistant.components.lock.*
|
||||
homeassistant.components.mailbox.*
|
||||
homeassistant.components.media_player.*
|
||||
homeassistant.components.modbus.*
|
||||
homeassistant.components.mysensors.*
|
||||
homeassistant.components.nam.*
|
||||
homeassistant.components.neato.*
|
||||
@@ -85,6 +89,7 @@ homeassistant.components.recorder.statistics
|
||||
homeassistant.components.remote.*
|
||||
homeassistant.components.renault.*
|
||||
homeassistant.components.rituals_perfume_genie.*
|
||||
homeassistant.components.samsungtv.*
|
||||
homeassistant.components.scene.*
|
||||
homeassistant.components.select.*
|
||||
homeassistant.components.sensor.*
|
||||
@@ -95,18 +100,23 @@ homeassistant.components.sonos.media_player
|
||||
homeassistant.components.ssdp.*
|
||||
homeassistant.components.stream.*
|
||||
homeassistant.components.sun.*
|
||||
homeassistant.components.surepetcare.*
|
||||
homeassistant.components.switch.*
|
||||
homeassistant.components.switcher_kis.*
|
||||
homeassistant.components.synology_dsm.*
|
||||
homeassistant.components.systemmonitor.*
|
||||
homeassistant.components.tag.*
|
||||
homeassistant.components.tautulli.*
|
||||
homeassistant.components.tcp.*
|
||||
homeassistant.components.tile.*
|
||||
homeassistant.components.tplink.*
|
||||
homeassistant.components.tradfri.*
|
||||
homeassistant.components.tts.*
|
||||
homeassistant.components.upcloud.*
|
||||
homeassistant.components.uptime.*
|
||||
homeassistant.components.uptimerobot.*
|
||||
homeassistant.components.vacuum.*
|
||||
homeassistant.components.vallox.*
|
||||
homeassistant.components.water_heater.*
|
||||
homeassistant.components.weather.*
|
||||
homeassistant.components.websocket_api.*
|
||||
|
||||
+16
-6
@@ -29,6 +29,7 @@ homeassistant/components/aemet/* @noltari
|
||||
homeassistant/components/agent_dvr/* @ispysoftware
|
||||
homeassistant/components/airly/* @bieniu
|
||||
homeassistant/components/airnow/* @asymworks
|
||||
homeassistant/components/airthings/* @danielhiversen
|
||||
homeassistant/components/airtouch4/* @LonePurpleWolf
|
||||
homeassistant/components/airvisual/* @bachya
|
||||
homeassistant/components/alarmdecoder/* @ajschmidt8
|
||||
@@ -36,6 +37,7 @@ homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy
|
||||
homeassistant/components/almond/* @gcampax @balloob
|
||||
homeassistant/components/alpha_vantage/* @fabaff
|
||||
homeassistant/components/ambee/* @frenck
|
||||
homeassistant/components/amberelectric/* @madpilot
|
||||
homeassistant/components/ambiclimate/* @danielhiversen
|
||||
homeassistant/components/ambient_station/* @bachya
|
||||
homeassistant/components/amcrest/* @flacjacket
|
||||
@@ -73,7 +75,7 @@ homeassistant/components/blink/* @fronzbot
|
||||
homeassistant/components/blueprint/* @home-assistant/core
|
||||
homeassistant/components/bmp280/* @belidzs
|
||||
homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe
|
||||
homeassistant/components/bond/* @prystupa
|
||||
homeassistant/components/bond/* @prystupa @joshs85
|
||||
homeassistant/components/bosch_shc/* @tschamm
|
||||
homeassistant/components/braviatv/* @bieniu @Drafteed
|
||||
homeassistant/components/broadlink/* @danielhiversen @felipediel
|
||||
@@ -104,6 +106,7 @@ homeassistant/components/coronavirus/* @home-assistant/core
|
||||
homeassistant/components/counter/* @fabaff
|
||||
homeassistant/components/cover/* @home-assistant/core
|
||||
homeassistant/components/cpuspeed/* @fabaff
|
||||
homeassistant/components/crownstone/* @Crownstone @RicArch97
|
||||
homeassistant/components/cups/* @fabaff
|
||||
homeassistant/components/daikin/* @fredrike
|
||||
homeassistant/components/darksky/* @fabaff
|
||||
@@ -120,6 +123,7 @@ homeassistant/components/dhcp/* @bdraco
|
||||
homeassistant/components/dht/* @thegardenmonkey
|
||||
homeassistant/components/digital_ocean/* @fabaff
|
||||
homeassistant/components/discogs/* @thibmaek
|
||||
homeassistant/components/dlna_dmr/* @StevenLooman @chishm
|
||||
homeassistant/components/doorbird/* @oblogic7 @bdraco
|
||||
homeassistant/components/dsmr/* @Robbie1221 @frenck
|
||||
homeassistant/components/dsmr_reader/* @depl0y
|
||||
@@ -132,6 +136,7 @@ homeassistant/components/ecobee/* @marthoc
|
||||
homeassistant/components/econet/* @vangorra @w1ll1am23
|
||||
homeassistant/components/ecovacs/* @OverloadUT
|
||||
homeassistant/components/edl21/* @mtdcr
|
||||
homeassistant/components/efergy/* @tkdrob
|
||||
homeassistant/components/egardia/* @jeroenterheerdt
|
||||
homeassistant/components/eight_sleep/* @mezz64
|
||||
homeassistant/components/elgato/* @frenck
|
||||
@@ -202,7 +207,7 @@ homeassistant/components/group/* @home-assistant/core
|
||||
homeassistant/components/growatt_server/* @indykoning @muppet3000 @JasperPlant
|
||||
homeassistant/components/guardian/* @bachya
|
||||
homeassistant/components/habitica/* @ASMfreaK @leikoilja
|
||||
homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey
|
||||
homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan
|
||||
homeassistant/components/hassio/* @home-assistant/supervisor
|
||||
homeassistant/components/heatmiser/* @andylockran
|
||||
homeassistant/components/heos/* @andrewsayre
|
||||
@@ -248,6 +253,7 @@ homeassistant/components/integration/* @dgomes
|
||||
homeassistant/components/intent/* @home-assistant/core
|
||||
homeassistant/components/intesishome/* @jnimmo
|
||||
homeassistant/components/ios/* @robbiet480
|
||||
homeassistant/components/iotawatt/* @gtdiehl @jyavenard
|
||||
homeassistant/components/iperf3/* @rohankapoorcom
|
||||
homeassistant/components/ipma/* @dgomes @abmantis
|
||||
homeassistant/components/ipp/* @ctalkington
|
||||
@@ -262,7 +268,7 @@ homeassistant/components/kaiterra/* @Michsior14
|
||||
homeassistant/components/keba/* @dannerph
|
||||
homeassistant/components/keenetic_ndms2/* @foxel
|
||||
homeassistant/components/kef/* @basnijholt
|
||||
homeassistant/components/keyboard_remote/* @bendavid
|
||||
homeassistant/components/keyboard_remote/* @bendavid @lanrat
|
||||
homeassistant/components/kmtronic/* @dgomes
|
||||
homeassistant/components/knx/* @Julius2342 @farmio @marvin-w
|
||||
homeassistant/components/kodi/* @OnFreund @cgtobi
|
||||
@@ -311,6 +317,7 @@ homeassistant/components/minecraft_server/* @elmurato
|
||||
homeassistant/components/minio/* @tkislan
|
||||
homeassistant/components/mobile_app/* @robbiet480
|
||||
homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik
|
||||
homeassistant/components/modem_callerid/* @tkdrob
|
||||
homeassistant/components/modern_forms/* @wonderslug
|
||||
homeassistant/components/monoprice/* @etsinko @OnFreund
|
||||
homeassistant/components/moon/* @fabaff
|
||||
@@ -334,6 +341,7 @@ homeassistant/components/ness_alarm/* @nickw444
|
||||
homeassistant/components/nest/* @allenporter
|
||||
homeassistant/components/netatmo/* @cgtobi
|
||||
homeassistant/components/netdata/* @fabaff
|
||||
homeassistant/components/netgear/* @hacf-fr @Quentame @starkillerOG
|
||||
homeassistant/components/nexia/* @bdraco
|
||||
homeassistant/components/nextbus/* @vividboarder
|
||||
homeassistant/components/nextcloud/* @meichthys
|
||||
@@ -499,7 +507,7 @@ homeassistant/components/supla/* @mwegrzynek
|
||||
homeassistant/components/surepetcare/* @benleb @danielhiversen
|
||||
homeassistant/components/swiss_hydrological_data/* @fabaff
|
||||
homeassistant/components/swiss_public_transport/* @fabaff
|
||||
homeassistant/components/switchbot/* @danielhiversen
|
||||
homeassistant/components/switchbot/* @danielhiversen @RenierM26
|
||||
homeassistant/components/switcher_kis/* @tomerfi @thecode
|
||||
homeassistant/components/switchmate/* @danielhiversen
|
||||
homeassistant/components/syncthing/* @zhulik
|
||||
@@ -517,7 +525,6 @@ homeassistant/components/tasmota/* @emontnemery
|
||||
homeassistant/components/tautulli/* @ludeeus
|
||||
homeassistant/components/tellduslive/* @fredrike
|
||||
homeassistant/components/template/* @PhracturedBlue @tetienne @home-assistant/core
|
||||
homeassistant/components/tesla/* @zabuldon @alandtse
|
||||
homeassistant/components/tfiac/* @fredrike @mellado
|
||||
homeassistant/components/thethingsnetwork/* @fabaff
|
||||
homeassistant/components/threshold/* @fabaff
|
||||
@@ -537,7 +544,7 @@ homeassistant/components/trafikverket_train/* @endor-force
|
||||
homeassistant/components/trafikverket_weatherstation/* @endor-force
|
||||
homeassistant/components/transmission/* @engrbm87 @JPHutchins
|
||||
homeassistant/components/tts/* @pvizeli
|
||||
homeassistant/components/tuya/* @ollo69
|
||||
homeassistant/components/tuya/* @Tuya
|
||||
homeassistant/components/twentemilieu/* @frenck
|
||||
homeassistant/components/twinkly/* @dr1rrb
|
||||
homeassistant/components/ubus/* @noltari
|
||||
@@ -552,6 +559,7 @@ homeassistant/components/uptimerobot/* @ludeeus
|
||||
homeassistant/components/usb/* @bdraco
|
||||
homeassistant/components/usgs_earthquakes_feed/* @exxamalte
|
||||
homeassistant/components/utility_meter/* @dgomes
|
||||
homeassistant/components/vallox/* @andre-richter
|
||||
homeassistant/components/velbus/* @Cereal2nd @brefra
|
||||
homeassistant/components/velux/* @Julius2342
|
||||
homeassistant/components/vera/* @pavoni
|
||||
@@ -570,10 +578,12 @@ homeassistant/components/wake_on_lan/* @ntilley905
|
||||
homeassistant/components/wallbox/* @hesselonline
|
||||
homeassistant/components/waqi/* @andrey-git
|
||||
homeassistant/components/watson_tts/* @rutkai
|
||||
homeassistant/components/watttime/* @bachya
|
||||
homeassistant/components/weather/* @fabaff
|
||||
homeassistant/components/webostv/* @bendavid @thecode
|
||||
homeassistant/components/websocket_api/* @home-assistant/core
|
||||
homeassistant/components/wemo/* @esev
|
||||
homeassistant/components/whirlpool/* @abmantis
|
||||
homeassistant/components/wiffi/* @mampfes
|
||||
homeassistant/components/wilight/* @leofig-rj
|
||||
homeassistant/components/wirelesstag/* @sergeymaysak
|
||||
|
||||
+15
@@ -16,6 +16,21 @@ RUN \
|
||||
-e ./homeassistant \
|
||||
&& python3 -m compileall homeassistant/homeassistant
|
||||
|
||||
# Fix Bug with Alpine 3.14 and sqlite 3.35
|
||||
# https://gitlab.alpinelinux.org/alpine/aports/-/issues/12524
|
||||
ARG BUILD_ARCH
|
||||
RUN \
|
||||
if [ "${BUILD_ARCH}" = "amd64" ]; then \
|
||||
export APK_ARCH=x86_64; \
|
||||
elif [ "${BUILD_ARCH}" = "i386" ]; then \
|
||||
export APK_ARCH=x86; \
|
||||
else \
|
||||
export APK_ARCH=${BUILD_ARCH}; \
|
||||
fi \
|
||||
&& curl -O http://dl-cdn.alpinelinux.org/alpine/v3.13/main/${APK_ARCH}/sqlite-libs-3.34.1-r0.apk \
|
||||
&& apk add --no-cache sqlite-libs-3.34.1-r0.apk \
|
||||
&& rm -f sqlite-libs-3.34.1-r0.apk
|
||||
|
||||
# Home Assistant S6-Overlay
|
||||
COPY rootfs /
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ RUN \
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||
&& apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
# Additional library needed by some tests and accordingly by VScode Tests Discovery
|
||||
bluez \
|
||||
libudev-dev \
|
||||
libavformat-dev \
|
||||
libavcodec-dev \
|
||||
|
||||
+6
-6
@@ -2,11 +2,11 @@
|
||||
"image": "homeassistant/{arch}-homeassistant",
|
||||
"shadow_repository": "ghcr.io/home-assistant",
|
||||
"build_from": {
|
||||
"aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.08.0",
|
||||
"armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.08.0",
|
||||
"armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.08.0",
|
||||
"amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.08.0",
|
||||
"i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.08.0"
|
||||
"aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.09.0",
|
||||
"armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.09.0",
|
||||
"armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.09.0",
|
||||
"amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.09.0",
|
||||
"i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.09.0"
|
||||
},
|
||||
"labels": {
|
||||
"io.hass.type": "core",
|
||||
@@ -19,4 +19,4 @@
|
||||
"org.opencontainers.image.licenses": "Apache License 2.0"
|
||||
},
|
||||
"version_tag": true
|
||||
}
|
||||
}
|
||||
@@ -118,14 +118,6 @@ homeassistant.util.pressure
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
homeassistant.util.ruamel\_yaml
|
||||
-------------------------------
|
||||
|
||||
.. automodule:: homeassistant.util.ruamel_yaml
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
homeassistant.util.ssl
|
||||
----------------------
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import faulthandler
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
@@ -10,6 +11,8 @@ import threading
|
||||
|
||||
from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
||||
|
||||
FAULT_LOG_FILENAME = "home-assistant.log.fault"
|
||||
|
||||
|
||||
def validate_python() -> None:
|
||||
"""Validate that the right Python version is running."""
|
||||
@@ -132,16 +135,14 @@ def get_arguments() -> argparse.Namespace:
|
||||
def daemonize() -> None:
|
||||
"""Move current process to daemon process."""
|
||||
# Create first fork
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
if os.fork() > 0:
|
||||
sys.exit(0)
|
||||
|
||||
# Decouple fork
|
||||
os.setsid()
|
||||
|
||||
# Create second fork
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
if os.fork() > 0:
|
||||
sys.exit(0)
|
||||
|
||||
# redirect standard file descriptors to devnull
|
||||
@@ -311,7 +312,15 @@ def main() -> int:
|
||||
open_ui=args.open_ui,
|
||||
)
|
||||
|
||||
exit_code = runner.run(runtime_conf)
|
||||
fault_file_name = os.path.join(config_dir, FAULT_LOG_FILENAME)
|
||||
with open(fault_file_name, mode="a", encoding="utf8") as fault_file:
|
||||
faulthandler.enable(fault_file)
|
||||
exit_code = runner.run(runtime_conf)
|
||||
faulthandler.disable()
|
||||
|
||||
if os.path.getsize(fault_file_name) == 0:
|
||||
os.remove(fault_file_name)
|
||||
|
||||
if exit_code == RESTART_EXIT_CODE and not args.runner:
|
||||
try_to_restart()
|
||||
|
||||
|
||||
@@ -341,8 +341,7 @@ class AuthManager:
|
||||
"System generated users cannot enable multi-factor auth module."
|
||||
)
|
||||
|
||||
module = self.get_auth_mfa_module(mfa_module_id)
|
||||
if module is None:
|
||||
if (module := self.get_auth_mfa_module(mfa_module_id)) is None:
|
||||
raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}")
|
||||
|
||||
await module.async_setup_user(user.id, data)
|
||||
@@ -356,8 +355,7 @@ class AuthManager:
|
||||
"System generated users cannot disable multi-factor auth module."
|
||||
)
|
||||
|
||||
module = self.get_auth_mfa_module(mfa_module_id)
|
||||
if module is None:
|
||||
if (module := self.get_auth_mfa_module(mfa_module_id)) is None:
|
||||
raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}")
|
||||
|
||||
await module.async_depose_user(user.id)
|
||||
@@ -466,7 +464,7 @@ class AuthManager:
|
||||
},
|
||||
refresh_token.jwt_key,
|
||||
algorithm="HS256",
|
||||
).decode()
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_resolve_provider(
|
||||
@@ -498,8 +496,7 @@ class AuthManager:
|
||||
|
||||
Will raise InvalidAuthError on errors.
|
||||
"""
|
||||
provider = self._async_resolve_provider(refresh_token)
|
||||
if provider:
|
||||
if provider := self._async_resolve_provider(refresh_token):
|
||||
provider.async_validate_refresh_token(refresh_token, remote_ip)
|
||||
|
||||
async def async_validate_access_token(
|
||||
@@ -507,7 +504,9 @@ class AuthManager:
|
||||
) -> models.RefreshToken | None:
|
||||
"""Return refresh token if an access token is valid."""
|
||||
try:
|
||||
unverif_claims = jwt.decode(token, verify=False)
|
||||
unverif_claims = jwt.decode(
|
||||
token, algorithms=["HS256"], options={"verify_signature": False}
|
||||
)
|
||||
except jwt.InvalidTokenError:
|
||||
return None
|
||||
|
||||
|
||||
@@ -96,8 +96,7 @@ class AuthStore:
|
||||
|
||||
groups = []
|
||||
for group_id in group_ids or []:
|
||||
group = self._groups.get(group_id)
|
||||
if group is None:
|
||||
if (group := self._groups.get(group_id)) is None:
|
||||
raise ValueError(f"Invalid group specified {group_id}")
|
||||
groups.append(group)
|
||||
|
||||
@@ -160,8 +159,7 @@ class AuthStore:
|
||||
if group_ids is not None:
|
||||
groups = []
|
||||
for grid in group_ids:
|
||||
group = self._groups.get(grid)
|
||||
if group is None:
|
||||
if (group := self._groups.get(grid)) is None:
|
||||
raise ValueError("Invalid group specified.")
|
||||
groups.append(group)
|
||||
|
||||
@@ -446,16 +444,14 @@ class AuthStore:
|
||||
)
|
||||
continue
|
||||
|
||||
token_type = rt_dict.get("token_type")
|
||||
if token_type is None:
|
||||
if (token_type := rt_dict.get("token_type")) is None:
|
||||
if rt_dict["client_id"] is None:
|
||||
token_type = models.TOKEN_TYPE_SYSTEM
|
||||
else:
|
||||
token_type = models.TOKEN_TYPE_NORMAL
|
||||
|
||||
# old refresh_token don't have last_used_at (pre-0.78)
|
||||
last_used_at_str = rt_dict.get("last_used_at")
|
||||
if last_used_at_str:
|
||||
if last_used_at_str := rt_dict.get("last_used_at"):
|
||||
last_used_at = dt_util.parse_datetime(last_used_at_str)
|
||||
else:
|
||||
last_used_at = None
|
||||
|
||||
@@ -118,9 +118,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
if self._user_settings is not None:
|
||||
return
|
||||
|
||||
data = await self._user_store.async_load()
|
||||
|
||||
if data is None:
|
||||
if (data := await self._user_store.async_load()) is None:
|
||||
data = {STORAGE_USERS: {}}
|
||||
|
||||
self._user_settings = {
|
||||
@@ -207,8 +205,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
await self._async_load()
|
||||
assert self._user_settings is not None
|
||||
|
||||
notify_setting = self._user_settings.get(user_id)
|
||||
if notify_setting is None:
|
||||
if (notify_setting := self._user_settings.get(user_id)) is None:
|
||||
return False
|
||||
|
||||
# user_input has been validate in caller
|
||||
@@ -225,8 +222,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
await self._async_load()
|
||||
assert self._user_settings is not None
|
||||
|
||||
notify_setting = self._user_settings.get(user_id)
|
||||
if notify_setting is None:
|
||||
if (notify_setting := self._user_settings.get(user_id)) is None:
|
||||
raise ValueError("Cannot find user_id")
|
||||
|
||||
def generate_secret_and_one_time_password() -> str:
|
||||
|
||||
@@ -92,9 +92,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
if self._users is not None:
|
||||
return
|
||||
|
||||
data = await self._user_store.async_load()
|
||||
|
||||
if data is None:
|
||||
if (data := await self._user_store.async_load()) is None:
|
||||
data = {STORAGE_USERS: {}}
|
||||
|
||||
self._users = data.get(STORAGE_USERS, {})
|
||||
@@ -163,8 +161,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
"""Validate two factor authentication code."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
|
||||
ota_secret = self._users.get(user_id) # type: ignore
|
||||
if ota_secret is None:
|
||||
if (ota_secret := self._users.get(user_id)) is None: # type: ignore
|
||||
# even we cannot find user, we still do verify
|
||||
# to make timing the same as if user was found.
|
||||
pyotp.TOTP(DUMMY_SECRET).verify(code, valid_window=1)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Permissions for Home Assistant."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import Any, Callable
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -33,9 +34,7 @@ class AbstractPermissions:
|
||||
|
||||
def check_entity(self, entity_id: str, key: str) -> bool:
|
||||
"""Check if we can access entity."""
|
||||
entity_func = self._cached_entity_func
|
||||
|
||||
if entity_func is None:
|
||||
if (entity_func := self._cached_entity_func) is None:
|
||||
entity_func = self._cached_entity_func = self._entity_func()
|
||||
|
||||
return entity_func(entity_id, key)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import Callable
|
||||
from collections.abc import Callable
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
||||
@@ -72,8 +72,7 @@ def compile_policy(
|
||||
def apply_policy_funcs(object_id: str, key: str) -> bool:
|
||||
"""Apply several policy functions."""
|
||||
for func in funcs:
|
||||
result = func(object_id, key)
|
||||
if result is not None:
|
||||
if (result := func(object_id, key)) is not None:
|
||||
return result
|
||||
return False
|
||||
|
||||
|
||||
@@ -169,9 +169,7 @@ async def load_auth_provider_module(
|
||||
if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"):
|
||||
return module
|
||||
|
||||
processed = hass.data.get(DATA_REQS)
|
||||
|
||||
if processed is None:
|
||||
if (processed := hass.data.get(DATA_REQS)) is None:
|
||||
processed = hass.data[DATA_REQS] = set()
|
||||
elif provider in processed:
|
||||
return module
|
||||
|
||||
@@ -82,9 +82,7 @@ class Data:
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Load stored data."""
|
||||
data = await self._store.async_load()
|
||||
|
||||
if data is None:
|
||||
if (data := await self._store.async_load()) is None:
|
||||
data = {"users": []}
|
||||
|
||||
seen: set[str] = set()
|
||||
@@ -93,9 +91,7 @@ class Data:
|
||||
username = user["username"]
|
||||
|
||||
# check if we have duplicates
|
||||
folded = username.casefold()
|
||||
|
||||
if folded in seen:
|
||||
if (folded := username.casefold()) in seen:
|
||||
self.is_legacy = True
|
||||
|
||||
logging.getLogger(__name__).warning(
|
||||
|
||||
+18
-22
@@ -109,9 +109,8 @@ async def async_setup_hass(
|
||||
|
||||
config_dict = None
|
||||
basic_setup_success = False
|
||||
safe_mode = runtime_config.safe_mode
|
||||
|
||||
if not safe_mode:
|
||||
if not (safe_mode := runtime_config.safe_mode):
|
||||
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
|
||||
|
||||
try:
|
||||
@@ -342,7 +341,11 @@ def async_enable_logging(
|
||||
err_log_path, backupCount=1
|
||||
)
|
||||
|
||||
err_handler.doRollover()
|
||||
try:
|
||||
err_handler.doRollover()
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error rolling over log file: %s", err)
|
||||
|
||||
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
|
||||
err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))
|
||||
|
||||
@@ -364,8 +367,7 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
|
||||
This function is a coroutine.
|
||||
"""
|
||||
deps_dir = os.path.join(config_dir, "deps")
|
||||
lib_dir = await async_get_user_site(deps_dir)
|
||||
if lib_dir not in sys.path:
|
||||
if (lib_dir := await async_get_user_site(deps_dir)) not in sys.path:
|
||||
sys.path.insert(0, lib_dir)
|
||||
return deps_dir
|
||||
|
||||
@@ -490,17 +492,13 @@ async def _async_set_up_integrations(
|
||||
|
||||
_LOGGER.info("Domains to be set up: %s", domains_to_setup)
|
||||
|
||||
logging_domains = domains_to_setup & LOGGING_INTEGRATIONS
|
||||
|
||||
# Load logging as soon as possible
|
||||
if logging_domains:
|
||||
if logging_domains := domains_to_setup & LOGGING_INTEGRATIONS:
|
||||
_LOGGER.info("Setting up logging: %s", logging_domains)
|
||||
await async_setup_multi_components(hass, logging_domains, config)
|
||||
|
||||
# Start up debuggers. Start these first in case they want to wait.
|
||||
debuggers = domains_to_setup & DEBUGGER_INTEGRATIONS
|
||||
|
||||
if debuggers:
|
||||
if debuggers := domains_to_setup & DEBUGGER_INTEGRATIONS:
|
||||
_LOGGER.debug("Setting up debuggers: %s", debuggers)
|
||||
await async_setup_multi_components(hass, debuggers, config)
|
||||
|
||||
@@ -520,9 +518,7 @@ async def _async_set_up_integrations(
|
||||
|
||||
stage_1_domains.add(domain)
|
||||
|
||||
dep_itg = integration_cache.get(domain)
|
||||
|
||||
if dep_itg is None:
|
||||
if (dep_itg := integration_cache.get(domain)) is None:
|
||||
continue
|
||||
|
||||
deps_promotion.update(dep_itg.all_dependencies)
|
||||
@@ -560,6 +556,14 @@ async def _async_set_up_integrations(
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.warning("Setup timed out for stage 2 - moving forward")
|
||||
|
||||
# Wrap up startup
|
||||
_LOGGER.debug("Waiting for startup to wrap up")
|
||||
try:
|
||||
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
|
||||
await hass.async_block_till_done()
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.warning("Setup timed out for bootstrap - moving forward")
|
||||
|
||||
watch_task.cancel()
|
||||
async_dispatcher_send(hass, SIGNAL_BOOTSTRAP_INTEGRATONS, {})
|
||||
|
||||
@@ -572,11 +576,3 @@ async def _async_set_up_integrations(
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
# Wrap up startup
|
||||
_LOGGER.debug("Waiting for startup to wrap up")
|
||||
try:
|
||||
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
|
||||
await hass.async_block_till_done()
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.warning("Setup timed out for bootstrap - moving forward")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "La r\u00e9-authentification a r\u00e9ussi",
|
||||
"single_instance_allowed": "D\u00e9ja configur\u00e9. Une seule configuration possible."
|
||||
"single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u00c9chec de connexion",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"name": "Nom de l'int\u00e9gration"
|
||||
"name": "Nom"
|
||||
},
|
||||
"description": "Si vous avez besoin d'aide pour la configuration, consultez le site suivant : https://www.home-assistant.io/integrations/accuweather/\n\nCertains capteurs ne sont pas activ\u00e9s par d\u00e9faut. Vous pouvez les activer dans le registre des entit\u00e9s apr\u00e8s la configuration de l'int\u00e9gration.\nLes pr\u00e9visions m\u00e9t\u00e9orologiques ne sont pas activ\u00e9es par d\u00e9faut. Vous pouvez l'activer dans les options d'int\u00e9gration.",
|
||||
"title": "AccuWeather"
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"account_id": "ID de la cuenta"
|
||||
"account_id": "ID de la cuenta",
|
||||
"host": "Anfitri\u00f3n",
|
||||
"password": "Contrase\u00f1a"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"host": "H\u00f4te",
|
||||
"password": "Mot de passe",
|
||||
"port": "Port",
|
||||
"ssl": "AdGuard Home utilise un certificat SSL",
|
||||
"ssl": "Utilise un certificat SSL",
|
||||
"username": "Nom d'utilisateur",
|
||||
"verify_ssl": "AdGuard Home utilise un certificat appropri\u00e9"
|
||||
"verify_ssl": "V\u00e9rifier le certificat SSL"
|
||||
},
|
||||
"description": "Configurez votre instance AdGuard Home pour permettre la surveillance et le contr\u00f4le."
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
"""Sensor platform for Advantage Air integration."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity
|
||||
from homeassistant.components.sensor import (
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
|
||||
@@ -138,11 +142,11 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity):
|
||||
|
||||
|
||||
class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity):
|
||||
"""Representation of Advantage Air Zone wireless signal sensor."""
|
||||
"""Representation of Advantage Air Zone temperature sensor."""
|
||||
|
||||
_attr_native_unit_of_measurement = TEMP_CELSIUS
|
||||
_attr_device_class = DEVICE_CLASS_TEMPERATURE
|
||||
_attr_state_class = STATE_CLASS_MEASUREMENT
|
||||
_attr_icon = "mdi:thermometer"
|
||||
_attr_entity_registry_enabled_default = False
|
||||
|
||||
def __init__(self, instance, ac_key, zone_key):
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Constant values for the AEMET OpenData component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import SensorEntityDescription
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
ATTR_CONDITION_CLOUDY,
|
||||
@@ -40,9 +42,6 @@ DEFAULT_NAME = "AEMET"
|
||||
DOMAIN = "aemet"
|
||||
ENTRY_NAME = "name"
|
||||
ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
|
||||
SENSOR_NAME = "sensor_name"
|
||||
SENSOR_UNIT = "sensor_unit"
|
||||
SENSOR_DEVICE_CLASS = "sensor_device_class"
|
||||
|
||||
ATTR_API_CONDITION = "condition"
|
||||
ATTR_API_FORECAST_DAILY = "forecast-daily"
|
||||
@@ -200,118 +199,145 @@ FORECAST_MODE_ATTR_API = {
|
||||
FORECAST_MODE_HOURLY: ATTR_API_FORECAST_HOURLY,
|
||||
}
|
||||
|
||||
FORECAST_SENSOR_TYPES = {
|
||||
ATTR_FORECAST_CONDITION: {
|
||||
SENSOR_NAME: "Condition",
|
||||
},
|
||||
ATTR_FORECAST_PRECIPITATION: {
|
||||
SENSOR_NAME: "Precipitation",
|
||||
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: {
|
||||
SENSOR_NAME: "Precipitation probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_FORECAST_TEMP: {
|
||||
SENSOR_NAME: "Temperature",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_FORECAST_TEMP_LOW: {
|
||||
SENSOR_NAME: "Temperature Low",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_FORECAST_TIME: {
|
||||
SENSOR_NAME: "Time",
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
},
|
||||
ATTR_FORECAST_WIND_BEARING: {
|
||||
SENSOR_NAME: "Wind bearing",
|
||||
SENSOR_UNIT: DEGREE,
|
||||
},
|
||||
ATTR_FORECAST_WIND_SPEED: {
|
||||
SENSOR_NAME: "Wind speed",
|
||||
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||
},
|
||||
}
|
||||
WEATHER_SENSOR_TYPES = {
|
||||
ATTR_API_CONDITION: {
|
||||
SENSOR_NAME: "Condition",
|
||||
},
|
||||
ATTR_API_HUMIDITY: {
|
||||
SENSOR_NAME: "Humidity",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
|
||||
},
|
||||
ATTR_API_PRESSURE: {
|
||||
SENSOR_NAME: "Pressure",
|
||||
SENSOR_UNIT: PRESSURE_HPA,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE,
|
||||
},
|
||||
ATTR_API_RAIN: {
|
||||
SENSOR_NAME: "Rain",
|
||||
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_API_RAIN_PROB: {
|
||||
SENSOR_NAME: "Rain probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_API_SNOW: {
|
||||
SENSOR_NAME: "Snow",
|
||||
SENSOR_UNIT: PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_API_SNOW_PROB: {
|
||||
SENSOR_NAME: "Snow probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_API_STATION_ID: {
|
||||
SENSOR_NAME: "Station ID",
|
||||
},
|
||||
ATTR_API_STATION_NAME: {
|
||||
SENSOR_NAME: "Station name",
|
||||
},
|
||||
ATTR_API_STATION_TIMESTAMP: {
|
||||
SENSOR_NAME: "Station timestamp",
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
},
|
||||
ATTR_API_STORM_PROB: {
|
||||
SENSOR_NAME: "Storm probability",
|
||||
SENSOR_UNIT: PERCENTAGE,
|
||||
},
|
||||
ATTR_API_TEMPERATURE: {
|
||||
SENSOR_NAME: "Temperature",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_API_TEMPERATURE_FEELING: {
|
||||
SENSOR_NAME: "Temperature feeling",
|
||||
SENSOR_UNIT: TEMP_CELSIUS,
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
ATTR_API_TOWN_ID: {
|
||||
SENSOR_NAME: "Town ID",
|
||||
},
|
||||
ATTR_API_TOWN_NAME: {
|
||||
SENSOR_NAME: "Town name",
|
||||
},
|
||||
ATTR_API_TOWN_TIMESTAMP: {
|
||||
SENSOR_NAME: "Town timestamp",
|
||||
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
},
|
||||
ATTR_API_WIND_BEARING: {
|
||||
SENSOR_NAME: "Wind bearing",
|
||||
SENSOR_UNIT: DEGREE,
|
||||
},
|
||||
ATTR_API_WIND_MAX_SPEED: {
|
||||
SENSOR_NAME: "Wind max speed",
|
||||
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||
},
|
||||
ATTR_API_WIND_SPEED: {
|
||||
SENSOR_NAME: "Wind speed",
|
||||
SENSOR_UNIT: SPEED_KILOMETERS_PER_HOUR,
|
||||
},
|
||||
}
|
||||
FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key=ATTR_FORECAST_CONDITION,
|
||||
name="Condition",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_FORECAST_PRECIPITATION,
|
||||
name="Precipitation",
|
||||
native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||
name="Precipitation probability",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_FORECAST_TEMP,
|
||||
name="Temperature",
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_FORECAST_TEMP_LOW,
|
||||
name="Temperature Low",
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_FORECAST_TIME,
|
||||
name="Time",
|
||||
device_class=DEVICE_CLASS_TIMESTAMP,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_FORECAST_WIND_BEARING,
|
||||
name="Wind bearing",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_FORECAST_WIND_SPEED,
|
||||
name="Wind speed",
|
||||
native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
|
||||
),
|
||||
)
|
||||
WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_CONDITION,
|
||||
name="Condition",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_HUMIDITY,
|
||||
name="Humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_PRESSURE,
|
||||
name="Pressure",
|
||||
native_unit_of_measurement=PRESSURE_HPA,
|
||||
device_class=DEVICE_CLASS_PRESSURE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_RAIN,
|
||||
name="Rain",
|
||||
native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_RAIN_PROB,
|
||||
name="Rain probability",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_SNOW,
|
||||
name="Snow",
|
||||
native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_SNOW_PROB,
|
||||
name="Snow probability",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_STATION_ID,
|
||||
name="Station ID",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_STATION_NAME,
|
||||
name="Station name",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_STATION_TIMESTAMP,
|
||||
name="Station timestamp",
|
||||
device_class=DEVICE_CLASS_TIMESTAMP,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_STORM_PROB,
|
||||
name="Storm probability",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_TEMPERATURE,
|
||||
name="Temperature",
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_TEMPERATURE_FEELING,
|
||||
name="Temperature feeling",
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_TOWN_ID,
|
||||
name="Town ID",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_TOWN_NAME,
|
||||
name="Town name",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_TOWN_TIMESTAMP,
|
||||
name="Town timestamp",
|
||||
device_class=DEVICE_CLASS_TIMESTAMP,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_WIND_BEARING,
|
||||
name="Wind bearing",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_WIND_MAX_SPEED,
|
||||
name="Wind max speed",
|
||||
native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_WIND_SPEED,
|
||||
name="Wind speed",
|
||||
native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
|
||||
),
|
||||
)
|
||||
|
||||
WIND_BEARING_MAP = {
|
||||
"C": None,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Support for the AEMET OpenData service."""
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -14,9 +16,6 @@ from .const import (
|
||||
FORECAST_MONITORED_CONDITIONS,
|
||||
FORECAST_SENSOR_TYPES,
|
||||
MONITORED_CONDITIONS,
|
||||
SENSOR_DEVICE_CLASS,
|
||||
SENSOR_NAME,
|
||||
SENSOR_UNIT,
|
||||
WEATHER_SENSOR_TYPES,
|
||||
)
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
@@ -28,37 +27,30 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
name = domain_data[ENTRY_NAME]
|
||||
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
||||
|
||||
weather_sensor_types = WEATHER_SENSOR_TYPES
|
||||
forecast_sensor_types = FORECAST_SENSOR_TYPES
|
||||
|
||||
entities = []
|
||||
for sensor_type in MONITORED_CONDITIONS:
|
||||
unique_id = f"{config_entry.unique_id}-{sensor_type}"
|
||||
entities.append(
|
||||
AemetSensor(
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
weather_sensor_types[sensor_type],
|
||||
unique_id = config_entry.unique_id
|
||||
entities: list[AbstractAemetSensor] = [
|
||||
AemetSensor(name, unique_id, weather_coordinator, description)
|
||||
for description in WEATHER_SENSOR_TYPES
|
||||
if description.key in MONITORED_CONDITIONS
|
||||
]
|
||||
entities.extend(
|
||||
[
|
||||
AemetForecastSensor(
|
||||
name_prefix,
|
||||
unique_id_prefix,
|
||||
weather_coordinator,
|
||||
mode,
|
||||
description,
|
||||
)
|
||||
)
|
||||
|
||||
for mode in FORECAST_MODES:
|
||||
name = f"{domain_data[ENTRY_NAME]} {mode}"
|
||||
|
||||
for sensor_type in FORECAST_MONITORED_CONDITIONS:
|
||||
unique_id = f"{config_entry.unique_id}-forecast-{mode}-{sensor_type}"
|
||||
entities.append(
|
||||
AemetForecastSensor(
|
||||
f"{name} Forecast",
|
||||
unique_id,
|
||||
sensor_type,
|
||||
forecast_sensor_types[sensor_type],
|
||||
weather_coordinator,
|
||||
mode,
|
||||
)
|
||||
for mode in FORECAST_MODES
|
||||
if (
|
||||
(name_prefix := f"{domain_data[ENTRY_NAME]} {mode} Forecast")
|
||||
and (unique_id_prefix := f"{unique_id}-forecast-{mode}")
|
||||
)
|
||||
for description in FORECAST_SENSOR_TYPES
|
||||
if description.key in FORECAST_MONITORED_CONDITIONS
|
||||
]
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -72,20 +64,14 @@ class AbstractAemetSensor(CoordinatorEntity, SensorEntity):
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
sensor_configuration,
|
||||
coordinator: WeatherUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._name = name
|
||||
self._unique_id = unique_id
|
||||
self._sensor_type = sensor_type
|
||||
self._sensor_name = sensor_configuration[SENSOR_NAME]
|
||||
self._attr_name = f"{self._name} {self._sensor_name}"
|
||||
self._attr_unique_id = self._unique_id
|
||||
self._attr_device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS)
|
||||
self._attr_native_unit_of_measurement = sensor_configuration.get(SENSOR_UNIT)
|
||||
self.entity_description = description
|
||||
self._attr_name = f"{name} {description.name}"
|
||||
self._attr_unique_id = unique_id
|
||||
|
||||
|
||||
class AemetSensor(AbstractAemetSensor):
|
||||
@@ -95,20 +81,21 @@ class AemetSensor(AbstractAemetSensor):
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
sensor_configuration,
|
||||
weather_coordinator: WeatherUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(
|
||||
name, unique_id, sensor_type, sensor_configuration, weather_coordinator
|
||||
name=name,
|
||||
unique_id=f"{unique_id}-{description.key}",
|
||||
coordinator=weather_coordinator,
|
||||
description=description,
|
||||
)
|
||||
self._weather_coordinator = weather_coordinator
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the device."""
|
||||
return self._weather_coordinator.data.get(self._sensor_type)
|
||||
return self.coordinator.data.get(self.entity_description.key)
|
||||
|
||||
|
||||
class AemetForecastSensor(AbstractAemetSensor):
|
||||
@@ -118,16 +105,17 @@ class AemetForecastSensor(AbstractAemetSensor):
|
||||
self,
|
||||
name,
|
||||
unique_id,
|
||||
sensor_type,
|
||||
sensor_configuration,
|
||||
weather_coordinator: WeatherUpdateCoordinator,
|
||||
forecast_mode,
|
||||
description: SensorEntityDescription,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(
|
||||
name, unique_id, sensor_type, sensor_configuration, weather_coordinator
|
||||
name=name,
|
||||
unique_id=f"{unique_id}-{description.key}",
|
||||
coordinator=weather_coordinator,
|
||||
description=description,
|
||||
)
|
||||
self._weather_coordinator = weather_coordinator
|
||||
self._forecast_mode = forecast_mode
|
||||
self._attr_entity_registry_enabled_default = (
|
||||
self._forecast_mode == FORECAST_MODE_DAILY
|
||||
@@ -137,9 +125,9 @@ class AemetForecastSensor(AbstractAemetSensor):
|
||||
def native_value(self):
|
||||
"""Return the state of the device."""
|
||||
forecast = None
|
||||
forecasts = self._weather_coordinator.data.get(
|
||||
forecasts = self.coordinator.data.get(
|
||||
FORECAST_MODE_ATTR_API[self._forecast_mode]
|
||||
)
|
||||
if forecasts:
|
||||
forecast = forecasts[0].get(self._sensor_type)
|
||||
forecast = forecasts[0].get(self.entity_description.key)
|
||||
return forecast
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
|
||||
},
|
||||
"error": {
|
||||
"already_in_progress": "La configuration de l'appareil est d\u00e9j\u00e0 en cours.",
|
||||
"already_in_progress": "La configuration est d\u00e9j\u00e0 en cours",
|
||||
"cannot_connect": "\u00c9chec de connexion"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Nom d'h\u00f4te ou adresse IP",
|
||||
"host": "H\u00f4te",
|
||||
"port": "Port"
|
||||
},
|
||||
"title": "Configurer l'agent DVR"
|
||||
|
||||
@@ -3,23 +3,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PM1,
|
||||
DEVICE_CLASS_PM10,
|
||||
DEVICE_CLASS_PM25,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
PERCENTAGE,
|
||||
PRESSURE_HPA,
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
|
||||
from .model import AirlySensorEntityDescription
|
||||
|
||||
ATTR_API_ADVICE: Final = "ADVICE"
|
||||
ATTR_API_CAQI: Final = "CAQI"
|
||||
ATTR_API_CAQI_DESCRIPTION: Final = "DESCRIPTION"
|
||||
@@ -49,56 +32,3 @@ MANUFACTURER: Final = "Airly sp. z o.o."
|
||||
MAX_UPDATE_INTERVAL: Final = 90
|
||||
MIN_UPDATE_INTERVAL: Final = 5
|
||||
NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet."
|
||||
|
||||
SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
|
||||
AirlySensorEntityDescription(
|
||||
key=ATTR_API_CAQI,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
name=ATTR_API_CAQI,
|
||||
native_unit_of_measurement="CAQI",
|
||||
),
|
||||
AirlySensorEntityDescription(
|
||||
key=ATTR_API_PM1,
|
||||
device_class=DEVICE_CLASS_PM1,
|
||||
name=ATTR_API_PM1,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
AirlySensorEntityDescription(
|
||||
key=ATTR_API_PM25,
|
||||
device_class=DEVICE_CLASS_PM25,
|
||||
name="PM2.5",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
AirlySensorEntityDescription(
|
||||
key=ATTR_API_PM10,
|
||||
device_class=DEVICE_CLASS_PM10,
|
||||
name=ATTR_API_PM10,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
AirlySensorEntityDescription(
|
||||
key=ATTR_API_HUMIDITY,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
name=ATTR_API_HUMIDITY.capitalize(),
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
value=lambda value: round(value, 1),
|
||||
),
|
||||
AirlySensorEntityDescription(
|
||||
key=ATTR_API_PRESSURE,
|
||||
device_class=DEVICE_CLASS_PRESSURE,
|
||||
name=ATTR_API_PRESSURE.capitalize(),
|
||||
native_unit_of_measurement=PRESSURE_HPA,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
AirlySensorEntityDescription(
|
||||
key=ATTR_API_TEMPERATURE,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
name=ATTR_API_TEMPERATURE.capitalize(),
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
value=lambda value: round(value, 1),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
"""Type definitions for Airly integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant.components.sensor import SensorEntityDescription
|
||||
|
||||
|
||||
@dataclass
|
||||
class AirlySensorEntityDescription(SensorEntityDescription):
|
||||
"""Class describing Airly sensor entities."""
|
||||
|
||||
value: Callable = round
|
||||
@@ -1,11 +1,31 @@
|
||||
"""Support for the Airly sensor service."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.components.sensor import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONF_NAME,
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PM1,
|
||||
DEVICE_CLASS_PM10,
|
||||
DEVICE_CLASS_PM25,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
PERCENTAGE,
|
||||
PRESSURE_HPA,
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
@@ -18,8 +38,12 @@ from .const import (
|
||||
ATTR_API_CAQI,
|
||||
ATTR_API_CAQI_DESCRIPTION,
|
||||
ATTR_API_CAQI_LEVEL,
|
||||
ATTR_API_HUMIDITY,
|
||||
ATTR_API_PM1,
|
||||
ATTR_API_PM10,
|
||||
ATTR_API_PM25,
|
||||
ATTR_API_PRESSURE,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_DESCRIPTION,
|
||||
ATTR_LEVEL,
|
||||
ATTR_LIMIT,
|
||||
@@ -28,15 +52,74 @@ from .const import (
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
SENSOR_TYPES,
|
||||
SUFFIX_LIMIT,
|
||||
SUFFIX_PERCENT,
|
||||
)
|
||||
from .model import AirlySensorEntityDescription
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass
|
||||
class AirlySensorEntityDescription(SensorEntityDescription):
|
||||
"""Class describing Airly sensor entities."""
|
||||
|
||||
value: Callable = round
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
|
||||
AirlySensorEntityDescription(
|
||||
key=ATTR_API_CAQI,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
name=ATTR_API_CAQI,
|
||||
native_unit_of_measurement="CAQI",
|
||||
),
|
||||
AirlySensorEntityDescription(
|
||||
key=ATTR_API_PM1,
|
||||
device_class=DEVICE_CLASS_PM1,
|
||||
name=ATTR_API_PM1,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
AirlySensorEntityDescription(
|
||||
key=ATTR_API_PM25,
|
||||
device_class=DEVICE_CLASS_PM25,
|
||||
name="PM2.5",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
AirlySensorEntityDescription(
|
||||
key=ATTR_API_PM10,
|
||||
device_class=DEVICE_CLASS_PM10,
|
||||
name=ATTR_API_PM10,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
AirlySensorEntityDescription(
|
||||
key=ATTR_API_HUMIDITY,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
name=ATTR_API_HUMIDITY.capitalize(),
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
value=lambda value: round(value, 1),
|
||||
),
|
||||
AirlySensorEntityDescription(
|
||||
key=ATTR_API_PRESSURE,
|
||||
device_class=DEVICE_CLASS_PRESSURE,
|
||||
name=ATTR_API_PRESSURE.capitalize(),
|
||||
native_unit_of_measurement=PRESSURE_HPA,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
AirlySensorEntityDescription(
|
||||
key=ATTR_API_TEMPERATURE,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
name=ATTR_API_TEMPERATURE.capitalize(),
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
value=lambda value: round(value, 1),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "L'int\u00e9gration des coordonn\u00e9es d'Airly est d\u00e9j\u00e0 configur\u00e9."
|
||||
"already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9"
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Cl\u00e9 API invalide",
|
||||
@@ -13,7 +13,7 @@
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"name": "Nom de l'int\u00e9gration"
|
||||
"name": "Nom"
|
||||
},
|
||||
"description": "Configurez l'int\u00e9gration de la qualit\u00e9 de l'air Airly. Pour g\u00e9n\u00e9rer une cl\u00e9 API, rendez-vous sur https://developer.airly.eu/register.",
|
||||
"title": "Airly"
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
"""Support for the AirNow sensor service."""
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ICON,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AirNowDataUpdateCoordinator
|
||||
from .const import (
|
||||
ATTR_API_AQI,
|
||||
ATTR_API_AQI_DESCRIPTION,
|
||||
@@ -22,69 +27,72 @@ from .const import (
|
||||
|
||||
ATTRIBUTION = "Data provided by AirNow"
|
||||
|
||||
ATTR_LABEL = "label"
|
||||
ATTR_UNIT = "unit"
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
SENSOR_TYPES = {
|
||||
ATTR_API_AQI: {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
ATTR_ICON: "mdi:blur",
|
||||
ATTR_LABEL: ATTR_API_AQI,
|
||||
ATTR_UNIT: "aqi",
|
||||
},
|
||||
ATTR_API_PM25: {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
ATTR_ICON: "mdi:blur",
|
||||
ATTR_LABEL: ATTR_API_PM25,
|
||||
ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
},
|
||||
ATTR_API_O3: {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
ATTR_ICON: "mdi:blur",
|
||||
ATTR_LABEL: ATTR_API_O3,
|
||||
ATTR_UNIT: CONCENTRATION_PARTS_PER_MILLION,
|
||||
},
|
||||
}
|
||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_AQI,
|
||||
icon="mdi:blur",
|
||||
name=ATTR_API_AQI,
|
||||
native_unit_of_measurement="aqi",
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_PM25,
|
||||
icon="mdi:blur",
|
||||
name=ATTR_API_PM25,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_O3,
|
||||
icon="mdi:blur",
|
||||
name=ATTR_API_O3,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up AirNow sensor entities based on a config entry."""
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
sensors = []
|
||||
for sensor in SENSOR_TYPES:
|
||||
sensors.append(AirNowSensor(coordinator, sensor))
|
||||
entities = [AirNowSensor(coordinator, description) for description in SENSOR_TYPES]
|
||||
|
||||
async_add_entities(sensors, False)
|
||||
async_add_entities(entities, False)
|
||||
|
||||
|
||||
class AirNowSensor(CoordinatorEntity, SensorEntity):
|
||||
"""Define an AirNow sensor."""
|
||||
|
||||
def __init__(self, coordinator, kind):
|
||||
coordinator: AirNowDataUpdateCoordinator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirNowDataUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self.kind = kind
|
||||
self.entity_description = description
|
||||
self._state = None
|
||||
self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||
self._attr_name = f"AirNow {SENSOR_TYPES[self.kind][ATTR_LABEL]}"
|
||||
self._attr_icon = SENSOR_TYPES[self.kind][ATTR_ICON]
|
||||
self._attr_device_class = SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
|
||||
self._attr_native_unit_of_measurement = SENSOR_TYPES[self.kind][ATTR_UNIT]
|
||||
self._attr_unique_id = f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}"
|
||||
self._attr_name = f"AirNow {description.name}"
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.latitude}-{coordinator.longitude}-{description.key.lower()}"
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state."""
|
||||
self._state = self.coordinator.data[self.kind]
|
||||
self._state = self.coordinator.data[self.entity_description.key]
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self.kind == ATTR_API_AQI:
|
||||
if self.entity_description.key == ATTR_API_AQI:
|
||||
self._attrs[SENSOR_AQI_ATTR_DESCR] = self.coordinator.data[
|
||||
ATTR_API_AQI_DESCRIPTION
|
||||
]
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u00c9chec \u00e0 la connexion",
|
||||
"cannot_connect": "\u00c9chec de connexion",
|
||||
"invalid_auth": "Authentification invalide",
|
||||
"invalid_location": "Aucun r\u00e9sultat trouv\u00e9 pour cet emplacement",
|
||||
"unknown": "Erreur inattendue"
|
||||
@@ -12,7 +12,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Cl\u00e9 API",
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"radius": "Rayon d'action de la station (en miles, facultatif)"
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""The Airthings integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from airthings import Airthings, AirthingsError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_ID, CONF_SECRET, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[str] = ["sensor"]
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Airthings from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
airthings = Airthings(
|
||||
entry.data[CONF_ID],
|
||||
entry.data[CONF_SECRET],
|
||||
async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
async def _update_method():
|
||||
"""Get the latest data from Airthings."""
|
||||
try:
|
||||
return await airthings.update_devices()
|
||||
except AirthingsError as err:
|
||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_method=_update_method,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Config flow for Airthings integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import airthings
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_ID, CONF_SECRET, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ID): str,
|
||||
vol.Required(CONF_SECRET): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Airthings."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
description_placeholders={
|
||||
"url": "https://dashboard.airthings.com/integrations/api-integration",
|
||||
},
|
||||
)
|
||||
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
await airthings.get_token(
|
||||
async_get_clientsession(self.hass),
|
||||
user_input[CONF_ID],
|
||||
user_input[CONF_SECRET],
|
||||
)
|
||||
except airthings.AirthingsConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except airthings.AirthingsAuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_ID])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(title="Airthings", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Constants for the Airthings integration."""
|
||||
|
||||
DOMAIN = "airthings"
|
||||
|
||||
CONF_ID = "id"
|
||||
CONF_SECRET = "secret"
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "airthings",
|
||||
"name": "Airthings",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airthings",
|
||||
"requirements": ["airthings_cloud==0.0.1"],
|
||||
"codeowners": [
|
||||
"@danielhiversen"
|
||||
],
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
"""Support for Airthings sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from airthings import AirthingsDevice
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
StateType,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_CO2,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PM1,
|
||||
DEVICE_CLASS_PM25,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
PERCENTAGE,
|
||||
PRESSURE_MBAR,
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"radonShortTermAvg": SensorEntityDescription(
|
||||
key="radonShortTermAvg",
|
||||
native_unit_of_measurement="Bq/m³",
|
||||
name="Radon",
|
||||
),
|
||||
"temp": SensorEntityDescription(
|
||||
key="temp",
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
name="Temperature",
|
||||
),
|
||||
"humidity": SensorEntityDescription(
|
||||
key="humidity",
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
name="Humidity",
|
||||
),
|
||||
"pressure": SensorEntityDescription(
|
||||
key="pressure",
|
||||
device_class=DEVICE_CLASS_PRESSURE,
|
||||
native_unit_of_measurement=PRESSURE_MBAR,
|
||||
name="Pressure",
|
||||
),
|
||||
"battery": SensorEntityDescription(
|
||||
key="battery",
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
name="Battery",
|
||||
),
|
||||
"co2": SensorEntityDescription(
|
||||
key="co2",
|
||||
device_class=DEVICE_CLASS_CO2,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
name="CO2",
|
||||
),
|
||||
"voc": SensorEntityDescription(
|
||||
key="voc",
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
|
||||
name="VOC",
|
||||
),
|
||||
"light": SensorEntityDescription(
|
||||
key="light",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
name="Light",
|
||||
),
|
||||
"virusRisk": SensorEntityDescription(
|
||||
key="virusRisk",
|
||||
name="Virus Risk",
|
||||
),
|
||||
"mold": SensorEntityDescription(
|
||||
key="mold",
|
||||
name="Mold",
|
||||
),
|
||||
"rssi": SensorEntityDescription(
|
||||
key="rssi",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
name="RSSI",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
"pm1": SensorEntityDescription(
|
||||
key="pm1",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=DEVICE_CLASS_PM1,
|
||||
name="PM1",
|
||||
),
|
||||
"pm25": SensorEntityDescription(
|
||||
key="pm25",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=DEVICE_CLASS_PM25,
|
||||
name="PM25",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Airthings sensor."""
|
||||
|
||||
coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
entities = [
|
||||
AirthingsHeaterEnergySensor(
|
||||
coordinator,
|
||||
airthings_device,
|
||||
SENSORS[sensor_types],
|
||||
)
|
||||
for airthings_device in coordinator.data.values()
|
||||
for sensor_types in airthings_device.sensor_types
|
||||
if sensor_types in SENSORS
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AirthingsHeaterEnergySensor(CoordinatorEntity, SensorEntity):
|
||||
"""Representation of a Airthings Sensor device."""
|
||||
|
||||
_attr_state_class = STATE_CLASS_MEASUREMENT
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
airthings_device: AirthingsDevice,
|
||||
entity_description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = entity_description
|
||||
|
||||
self._attr_name = f"{airthings_device.name} {entity_description.name}"
|
||||
self._attr_unique_id = f"{airthings_device.device_id}_{entity_description.key}"
|
||||
self._id = airthings_device.device_id
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, airthings_device.device_id)},
|
||||
"name": airthings_device.name,
|
||||
"manufacturer": "Airthings",
|
||||
}
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the value reported by the sensor."""
|
||||
return self.coordinator.data[self._id].sensors[self.entity_description.key]
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"id": "ID",
|
||||
"secret": "Secret",
|
||||
"description": "Login at {url} to find your credentials"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 {intergration}."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"no_units": "No se pudo encontrar ning\u00fan grupo AirTouch 4."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Anfitri\u00f3n"
|
||||
},
|
||||
"title": "Configura los detalles de conexi\u00f3n de tu AirTouch 4."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u00c9chec de connexion"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "H\u00f4te"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
"""Support for AirVisual air quality sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
from homeassistant.components.sensor import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
@@ -76,6 +80,7 @@ GEOGRAPHY_SENSOR_DESCRIPTIONS = (
|
||||
name="Air Quality Index",
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
native_unit_of_measurement="AQI",
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KIND_POLLUTANT,
|
||||
@@ -92,6 +97,7 @@ NODE_PRO_SENSOR_DESCRIPTIONS = (
|
||||
name="Air Quality Index",
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
native_unit_of_measurement="AQI",
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KIND_BATTERY_LEVEL,
|
||||
@@ -104,6 +110,7 @@ NODE_PRO_SENSOR_DESCRIPTIONS = (
|
||||
name="C02",
|
||||
device_class=DEVICE_CLASS_CO2,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KIND_HUMIDITY,
|
||||
@@ -116,30 +123,35 @@ NODE_PRO_SENSOR_DESCRIPTIONS = (
|
||||
name="PM 0.1",
|
||||
device_class=DEVICE_CLASS_PM1,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KIND_PM_1_0,
|
||||
name="PM 1.0",
|
||||
device_class=DEVICE_CLASS_PM10,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KIND_PM_2_5,
|
||||
name="PM 2.5",
|
||||
device_class=DEVICE_CLASS_PM25,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KIND_TEMPERATURE,
|
||||
name="Temperature",
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SENSOR_KIND_VOC,
|
||||
name="VOC",
|
||||
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"step": {
|
||||
"geography_by_coords": {
|
||||
"data": {
|
||||
"api_key": "Clef d'API",
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude"
|
||||
},
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"geography_by_name": {
|
||||
"data": {
|
||||
"api_key": "Clef d'API",
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"city": "Ville",
|
||||
"country": "Pays",
|
||||
"state": "Etat"
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
"s2": "Di\u00f3xido de azufre"
|
||||
},
|
||||
"airvisual__pollutant_level": {
|
||||
"good": "Bien",
|
||||
"hazardous": "Peligroso",
|
||||
"good": "Bueno",
|
||||
"hazardous": "Da\u00f1ino",
|
||||
"moderate": "Moderado",
|
||||
"unhealthy": "Insalubre",
|
||||
"unhealthy_sensitive": "Incorrecto para grupos sensibles",
|
||||
"unhealthy_sensitive": "Insalubre para grupos sensibles",
|
||||
"very_unhealthy": "Muy poco saludable"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"state": {
|
||||
"airvisual__pollutant_label": {
|
||||
"co": "Tlenek w\u0119gla",
|
||||
"n2": "Dwutlenek azotu",
|
||||
"o3": "Ozon",
|
||||
"co": "tlenek w\u0119gla",
|
||||
"n2": "dwutlenek azotu",
|
||||
"o3": "ozon",
|
||||
"p1": "PM10",
|
||||
"p2": "PM2.5",
|
||||
"s2": "Dwutlenek siarki"
|
||||
"s2": "dwutlenek siarki"
|
||||
},
|
||||
"airvisual__pollutant_level": {
|
||||
"good": "dobry",
|
||||
|
||||
@@ -11,7 +11,10 @@ from homeassistant.components.alarm_control_panel.const import (
|
||||
SUPPORT_ALARM_ARM_NIGHT,
|
||||
SUPPORT_ALARM_ARM_VACATION,
|
||||
)
|
||||
from homeassistant.components.automation import AutomationActionType
|
||||
from homeassistant.components.automation import (
|
||||
AutomationActionType,
|
||||
AutomationTriggerInfo,
|
||||
)
|
||||
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
|
||||
from homeassistant.components.homeassistant.triggers import state as state_trigger
|
||||
from homeassistant.const import (
|
||||
@@ -129,7 +132,7 @@ async def async_attach_trigger(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
action: AutomationActionType,
|
||||
automation_info: dict,
|
||||
automation_info: AutomationTriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
if config[CONF_TYPE] == "triggered":
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"arm_away": "Schakel {entity_name} in voor vertrek",
|
||||
"arm_home": "Schakel {entity_name} in voor thuis",
|
||||
"arm_night": "Schakel {entity_name} in voor 's nachts",
|
||||
"arm_vacation": "Schakel {entity_name} in op vakantie",
|
||||
"arm_vacation": "Schakel {entity_name} in voor vakantie",
|
||||
"disarm": "Schakel {entity_name} uit",
|
||||
"trigger": "Laat {entity_name} afgaan"
|
||||
},
|
||||
@@ -12,7 +12,7 @@
|
||||
"is_armed_away": "{entity_name} ingeschakeld voor vertrek",
|
||||
"is_armed_home": "{entity_name} ingeschakeld voor thuis",
|
||||
"is_armed_night": "{entity_name} is ingeschakeld voor 's nachts",
|
||||
"is_armed_vacation": "{entity_name} is in vakantie geschakeld",
|
||||
"is_armed_vacation": "{entity_name} is ingeschakeld voor vakantie",
|
||||
"is_disarmed": "{entity_name} is uitgeschakeld",
|
||||
"is_triggered": "{entity_name} gaat af"
|
||||
},
|
||||
@@ -20,7 +20,7 @@
|
||||
"armed_away": "{entity_name} ingeschakeld voor vertrek",
|
||||
"armed_home": "{entity_name} ingeschakeld voor thuis",
|
||||
"armed_night": "{entity_name} ingeschakeld voor 's nachts",
|
||||
"armed_vacation": "{entity_name} schakelde vakantie in",
|
||||
"armed_vacation": "{entity_name} schakelde in voor vakantie",
|
||||
"disarmed": "{entity_name} uitgeschakeld",
|
||||
"triggered": "{entity_name} afgegaan"
|
||||
}
|
||||
@@ -40,5 +40,5 @@
|
||||
"triggered": "Gaat af"
|
||||
}
|
||||
},
|
||||
"title": "Alarm bedieningspaneel"
|
||||
"title": "Alarmbedieningspaneel"
|
||||
}
|
||||
@@ -48,6 +48,7 @@ from .const import (
|
||||
API_THERMOSTAT_MODES,
|
||||
API_THERMOSTAT_PRESETS,
|
||||
DATE_FORMAT,
|
||||
PRESET_MODE_NA,
|
||||
Inputs,
|
||||
)
|
||||
from .errors import UnsupportedProperty
|
||||
@@ -391,6 +392,8 @@ class AlexaPowerController(AlexaCapability):
|
||||
|
||||
if self.entity.domain == climate.DOMAIN:
|
||||
is_on = self.entity.state != climate.HVAC_MODE_OFF
|
||||
elif self.entity.domain == fan.DOMAIN:
|
||||
is_on = self.entity.state == fan.STATE_ON
|
||||
elif self.entity.domain == vacuum.DOMAIN:
|
||||
is_on = self.entity.state == vacuum.STATE_CLEANING
|
||||
elif self.entity.domain == timer.DOMAIN:
|
||||
@@ -1155,9 +1158,6 @@ class AlexaPowerLevelController(AlexaCapability):
|
||||
if name != "powerLevel":
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if self.entity.domain == fan.DOMAIN:
|
||||
return self.entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
|
||||
|
||||
|
||||
class AlexaSecurityPanelController(AlexaCapability):
|
||||
"""Implements Alexa.SecurityPanelController.
|
||||
@@ -1354,10 +1354,17 @@ class AlexaModeController(AlexaCapability):
|
||||
self._resource = AlexaModeResource(
|
||||
[AlexaGlobalCatalog.SETTING_PRESET], False
|
||||
)
|
||||
for preset_mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, []):
|
||||
preset_modes = self.entity.attributes.get(fan.ATTR_PRESET_MODES, [])
|
||||
for preset_mode in preset_modes:
|
||||
self._resource.add_mode(
|
||||
f"{fan.ATTR_PRESET_MODE}.{preset_mode}", [preset_mode]
|
||||
)
|
||||
# Fans with a single preset_mode completely break Alexa discovery, add a
|
||||
# fake preset (see issue #53832).
|
||||
if len(preset_modes) == 1:
|
||||
self._resource.add_mode(
|
||||
f"{fan.ATTR_PRESET_MODE}.{PRESET_MODE_NA}", [PRESET_MODE_NA]
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Cover Position Resources
|
||||
@@ -1483,16 +1490,6 @@ class AlexaRangeController(AlexaCapability):
|
||||
if self.entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None):
|
||||
return None
|
||||
|
||||
# Fan Speed
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||
speed_list = self.entity.attributes.get(fan.ATTR_SPEED_LIST)
|
||||
speed = self.entity.attributes.get(fan.ATTR_SPEED)
|
||||
if speed_list is not None and speed is not None:
|
||||
speed_index = next(
|
||||
(i for i, v in enumerate(speed_list) if v == speed), None
|
||||
)
|
||||
return speed_index
|
||||
|
||||
# Cover Position
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION)
|
||||
@@ -1501,6 +1498,13 @@ class AlexaRangeController(AlexaCapability):
|
||||
if self.instance == f"{cover.DOMAIN}.tilt":
|
||||
return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION)
|
||||
|
||||
# Fan speed percentage
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported and fan.SUPPORT_SET_SPEED:
|
||||
return self.entity.attributes.get(fan.ATTR_PERCENTAGE)
|
||||
return 100 if self.entity.state == fan.STATE_ON else 0
|
||||
|
||||
# Input Number Value
|
||||
if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
|
||||
return float(self.entity.state)
|
||||
@@ -1527,28 +1531,16 @@ class AlexaRangeController(AlexaCapability):
|
||||
def capability_resources(self):
|
||||
"""Return capabilityResources object."""
|
||||
|
||||
# Fan Speed Resources
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||
speed_list = self.entity.attributes[fan.ATTR_SPEED_LIST]
|
||||
max_value = len(speed_list) - 1
|
||||
# Fan Speed Percentage Resources
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
|
||||
percentage_step = self.entity.attributes.get(fan.ATTR_PERCENTAGE_STEP)
|
||||
self._resource = AlexaPresetResource(
|
||||
labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED],
|
||||
labels=["Percentage", AlexaGlobalCatalog.SETTING_FAN_SPEED],
|
||||
min_value=0,
|
||||
max_value=max_value,
|
||||
precision=1,
|
||||
max_value=100,
|
||||
precision=percentage_step if percentage_step else 100,
|
||||
unit=AlexaGlobalCatalog.UNIT_PERCENT,
|
||||
)
|
||||
for index, speed in enumerate(speed_list):
|
||||
labels = []
|
||||
if isinstance(speed, str):
|
||||
labels.append(speed.replace("_", " "))
|
||||
if index == 1:
|
||||
labels.append(AlexaGlobalCatalog.VALUE_MINIMUM)
|
||||
if index == max_value:
|
||||
labels.append(AlexaGlobalCatalog.VALUE_MAXIMUM)
|
||||
|
||||
if len(labels) > 0:
|
||||
self._resource.add_preset(value=index, labels=labels)
|
||||
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Cover Position Resources
|
||||
@@ -1661,6 +1653,20 @@ class AlexaRangeController(AlexaCapability):
|
||||
)
|
||||
return self._semantics.serialize_semantics()
|
||||
|
||||
# Fan Speed Percentage
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
|
||||
lower_labels = [AlexaSemantics.ACTION_LOWER]
|
||||
raise_labels = [AlexaSemantics.ACTION_RAISE]
|
||||
self._semantics = AlexaSemantics()
|
||||
|
||||
self._semantics.add_action_to_directive(
|
||||
lower_labels, "SetRangeValue", {"rangeValue": 0}
|
||||
)
|
||||
self._semantics.add_action_to_directive(
|
||||
raise_labels, "SetRangeValue", {"rangeValue": 100}
|
||||
)
|
||||
return self._semantics.serialize_semantics()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -78,6 +78,9 @@ API_THERMOSTAT_MODES = OrderedDict(
|
||||
API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"}
|
||||
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
|
||||
|
||||
# AlexaModeController does not like a single mode for the fan preset, we add PRESET_MODE_NA if a fan has only one preset_mode
|
||||
PRESET_MODE_NA = "-"
|
||||
|
||||
|
||||
class Cause:
|
||||
"""Possible causes for property changes.
|
||||
|
||||
@@ -60,11 +60,9 @@ from .capabilities import (
|
||||
AlexaLockController,
|
||||
AlexaModeController,
|
||||
AlexaMotionSensor,
|
||||
AlexaPercentageController,
|
||||
AlexaPlaybackController,
|
||||
AlexaPlaybackStateReporter,
|
||||
AlexaPowerController,
|
||||
AlexaPowerLevelController,
|
||||
AlexaRangeController,
|
||||
AlexaSceneController,
|
||||
AlexaSecurityPanelController,
|
||||
@@ -530,27 +528,32 @@ class FanCapabilities(AlexaEntity):
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
yield AlexaPowerController(self.entity)
|
||||
|
||||
force_range_controller = True
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & fan.SUPPORT_SET_SPEED:
|
||||
yield AlexaPercentageController(self.entity)
|
||||
yield AlexaPowerLevelController(self.entity)
|
||||
# The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7)
|
||||
yield AlexaRangeController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}"
|
||||
)
|
||||
if supported & fan.SUPPORT_OSCILLATE:
|
||||
yield AlexaToggleController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
|
||||
)
|
||||
force_range_controller = False
|
||||
if supported & fan.SUPPORT_PRESET_MODE:
|
||||
yield AlexaModeController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}"
|
||||
)
|
||||
force_range_controller = False
|
||||
if supported & fan.SUPPORT_DIRECTION:
|
||||
yield AlexaModeController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}"
|
||||
)
|
||||
force_range_controller = False
|
||||
|
||||
# AlexaRangeController controls the Fan Speed Percentage.
|
||||
# For fans which only support on/off, no controller is added. This makes the
|
||||
# fan impossible to turn on or off through Alexa, most likely due to a bug in Alexa.
|
||||
# As a workaround, we add a range controller which can only be set to 0% or 100%.
|
||||
if force_range_controller or supported & fan.SUPPORT_SET_SPEED:
|
||||
yield AlexaRangeController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}"
|
||||
)
|
||||
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
||||
@@ -54,6 +54,8 @@ from .const import (
|
||||
API_THERMOSTAT_MODES,
|
||||
API_THERMOSTAT_MODES_CUSTOM,
|
||||
API_THERMOSTAT_PRESETS,
|
||||
DATE_FORMAT,
|
||||
PRESET_MODE_NA,
|
||||
Cause,
|
||||
Inputs,
|
||||
)
|
||||
@@ -122,6 +124,8 @@ async def async_api_turn_on(hass, config, directive, context):
|
||||
service = SERVICE_TURN_ON
|
||||
if domain == cover.DOMAIN:
|
||||
service = cover.SERVICE_OPEN_COVER
|
||||
elif domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_TURN_ON
|
||||
elif domain == vacuum.DOMAIN:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if not supported & vacuum.SUPPORT_TURN_ON and supported & vacuum.SUPPORT_START:
|
||||
@@ -156,6 +160,8 @@ async def async_api_turn_off(hass, config, directive, context):
|
||||
service = SERVICE_TURN_OFF
|
||||
if entity.domain == cover.DOMAIN:
|
||||
service = cover.SERVICE_CLOSE_COVER
|
||||
elif domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_TURN_OFF
|
||||
elif domain == vacuum.DOMAIN:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if (
|
||||
@@ -318,7 +324,7 @@ async def async_api_activate(hass, config, directive, context):
|
||||
|
||||
payload = {
|
||||
"cause": {"type": Cause.VOICE_INTERACTION},
|
||||
"timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z",
|
||||
"timestamp": dt_util.utcnow().strftime(DATE_FORMAT),
|
||||
}
|
||||
|
||||
return directive.response(
|
||||
@@ -342,7 +348,7 @@ async def async_api_deactivate(hass, config, directive, context):
|
||||
|
||||
payload = {
|
||||
"cause": {"type": Cause.VOICE_INTERACTION},
|
||||
"timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z",
|
||||
"timestamp": dt_util.utcnow().strftime(DATE_FORMAT),
|
||||
}
|
||||
|
||||
return directive.response(
|
||||
@@ -825,48 +831,6 @@ async def async_api_reportstate(hass, config, directive, context):
|
||||
return directive.response(name="StateReport")
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.PowerLevelController", "SetPowerLevel"))
|
||||
async def async_api_set_power_level(hass, config, directive, context):
|
||||
"""Process a SetPowerLevel request."""
|
||||
entity = directive.entity
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_SET_PERCENTAGE
|
||||
percentage = int(directive.payload["powerLevel"])
|
||||
data[fan.ATTR_PERCENTAGE] = percentage
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.PowerLevelController", "AdjustPowerLevel"))
|
||||
async def async_api_adjust_power_level(hass, config, directive, context):
|
||||
"""Process an AdjustPowerLevel request."""
|
||||
entity = directive.entity
|
||||
percentage_delta = int(directive.payload["powerLevelDelta"])
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_SET_PERCENTAGE
|
||||
current = entity.attributes.get(fan.ATTR_PERCENTAGE) or 0
|
||||
|
||||
# set percentage
|
||||
percentage = min(100, max(0, percentage_delta + current))
|
||||
data[fan.ATTR_PERCENTAGE] = percentage
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.SecurityPanelController", "Arm"))
|
||||
async def async_api_arm(hass, config, directive, context):
|
||||
"""Process a Security Panel Arm request."""
|
||||
@@ -961,7 +925,9 @@ async def async_api_set_mode(hass, config, directive, context):
|
||||
# Fan preset_mode
|
||||
elif instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
|
||||
preset_mode = mode.split(".")[1]
|
||||
if preset_mode in entity.attributes.get(fan.ATTR_PRESET_MODES):
|
||||
if preset_mode != PRESET_MODE_NA and preset_mode in entity.attributes.get(
|
||||
fan.ATTR_PRESET_MODES
|
||||
):
|
||||
service = fan.SERVICE_SET_PRESET_MODE
|
||||
data[fan.ATTR_PRESET_MODE] = preset_mode
|
||||
else:
|
||||
@@ -1091,24 +1057,8 @@ async def async_api_set_range(hass, config, directive, context):
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
range_value = directive.payload["rangeValue"]
|
||||
|
||||
# Fan Speed
|
||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||
range_value = int(range_value)
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
speed_list = entity.attributes[fan.ATTR_SPEED_LIST]
|
||||
speed = next((v for i, v in enumerate(speed_list) if i == range_value), None)
|
||||
|
||||
if not speed:
|
||||
msg = "Entity does not support value"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
|
||||
if speed == fan.SPEED_OFF:
|
||||
service = fan.SERVICE_TURN_OFF
|
||||
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
|
||||
# Cover Position
|
||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
range_value = int(range_value)
|
||||
if range_value == 0:
|
||||
service = cover.SERVICE_CLOSE_COVER
|
||||
@@ -1129,6 +1079,19 @@ async def async_api_set_range(hass, config, directive, context):
|
||||
service = cover.SERVICE_SET_COVER_TILT_POSITION
|
||||
data[cover.ATTR_TILT_POSITION] = range_value
|
||||
|
||||
# Fan Speed
|
||||
elif instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
|
||||
range_value = int(range_value)
|
||||
if range_value == 0:
|
||||
service = fan.SERVICE_TURN_OFF
|
||||
else:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported and fan.SUPPORT_SET_SPEED:
|
||||
service = fan.SERVICE_SET_PERCENTAGE
|
||||
data[fan.ATTR_PERCENTAGE] = range_value
|
||||
else:
|
||||
service = fan.SERVICE_TURN_ON
|
||||
|
||||
# Input Number Value
|
||||
elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
|
||||
range_value = float(range_value)
|
||||
@@ -1184,29 +1147,8 @@ async def async_api_adjust_range(hass, config, directive, context):
|
||||
range_delta_default = bool(directive.payload["rangeValueDeltaDefault"])
|
||||
response_value = 0
|
||||
|
||||
# Fan Speed
|
||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||
range_delta = int(range_delta)
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
speed_list = entity.attributes[fan.ATTR_SPEED_LIST]
|
||||
current_speed = entity.attributes[fan.ATTR_SPEED]
|
||||
current_speed_index = next(
|
||||
(i for i, v in enumerate(speed_list) if v == current_speed), 0
|
||||
)
|
||||
new_speed_index = min(
|
||||
len(speed_list) - 1, max(0, current_speed_index + range_delta)
|
||||
)
|
||||
speed = next(
|
||||
(v for i, v in enumerate(speed_list) if i == new_speed_index), None
|
||||
)
|
||||
|
||||
if speed == fan.SPEED_OFF:
|
||||
service = fan.SERVICE_TURN_OFF
|
||||
|
||||
data[fan.ATTR_SPEED] = response_value = speed
|
||||
|
||||
# Cover Position
|
||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
range_delta = int(range_delta * 20) if range_delta_default else int(range_delta)
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
current = entity.attributes.get(cover.ATTR_POSITION)
|
||||
@@ -1237,6 +1179,25 @@ async def async_api_adjust_range(hass, config, directive, context):
|
||||
else:
|
||||
data[cover.ATTR_TILT_POSITION] = tilt_position
|
||||
|
||||
# Fan speed percentage
|
||||
elif instance == f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}":
|
||||
percentage_step = entity.attributes.get(fan.ATTR_PERCENTAGE_STEP) or 20
|
||||
range_delta = (
|
||||
int(range_delta * percentage_step)
|
||||
if range_delta_default
|
||||
else int(range_delta)
|
||||
)
|
||||
service = fan.SERVICE_SET_PERCENTAGE
|
||||
current = entity.attributes.get(fan.ATTR_PERCENTAGE)
|
||||
if not current:
|
||||
msg = f"Unable to determine {entity.entity_id} current fan speed"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
percentage = response_value = min(100, max(0, range_delta + current))
|
||||
if percentage:
|
||||
data[fan.ATTR_PERCENTAGE] = percentage
|
||||
else:
|
||||
service = fan.SERVICE_TURN_OFF
|
||||
|
||||
# Input Number Value
|
||||
elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
|
||||
range_delta = float(range_delta)
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.helpers.significant_change import create_checker
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import API_CHANGE, DOMAIN, Cause
|
||||
from .const import API_CHANGE, DATE_FORMAT, DOMAIN, Cause
|
||||
from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id
|
||||
from .messages import AlexaResponse
|
||||
|
||||
@@ -252,7 +252,7 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity):
|
||||
namespace="Alexa.DoorbellEventSource",
|
||||
payload={
|
||||
"cause": {"type": Cause.PHYSICAL_INTERACTION},
|
||||
"timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z",
|
||||
"timestamp": dt_util.utcnow().strftime(DATE_FORMAT),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"cannot_connect": "Impossible de se connecter au serveur Almond",
|
||||
"missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond.",
|
||||
"cannot_connect": "\u00c9chec de connexion",
|
||||
"missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.",
|
||||
"no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )",
|
||||
"single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
|
||||
},
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "R\u00e9-authentification r\u00e9ussie"
|
||||
"reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u00c9chec de connexion",
|
||||
"invalid_api_key": "Cl\u00e9 API non valide"
|
||||
"invalid_api_key": "Cl\u00e9 API invalide"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "cl\u00e9 API",
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"description": "R\u00e9-authentifiez-vous avec votre compte Ambee."
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "cl\u00e9 API",
|
||||
"api_key": "Cl\u00e9 d'API",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"name": "Nom"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"state": {
|
||||
"ambee__risk": {
|
||||
"high": "Wysoki",
|
||||
"low": "Niski",
|
||||
"moderate": "Umiarkowany",
|
||||
"very high": "Bardzo wysoki"
|
||||
"high": "wysoki",
|
||||
"low": "niski",
|
||||
"moderate": "umiarkowany",
|
||||
"very high": "bardzo wysoki"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Support for Amber Electric."""
|
||||
|
||||
from amberelectric import Configuration
|
||||
from amberelectric.api import amber_api
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_API_TOKEN, CONF_SITE_ID, DOMAIN, PLATFORMS
|
||||
from .coordinator import AmberUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Amber Electric from a config entry."""
|
||||
configuration = Configuration(access_token=entry.data[CONF_API_TOKEN])
|
||||
api_instance = amber_api.AmberApi.create(configuration)
|
||||
site_id = entry.data[CONF_SITE_ID]
|
||||
|
||||
coordinator = AmberUpdateCoordinator(hass, api_instance, site_id)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Amber Electric Binary Sensor definitions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTRIBUTION, DOMAIN
|
||||
from .coordinator import AmberUpdateCoordinator
|
||||
|
||||
PRICE_SPIKE_ICONS = {
|
||||
"none": "mdi:power-plug",
|
||||
"potential": "mdi:power-plug-outline",
|
||||
"spike": "mdi:power-plug-off",
|
||||
}
|
||||
|
||||
|
||||
class AmberPriceGridSensor(CoordinatorEntity, BinarySensorEntity):
|
||||
"""Sensor to show single grid binary values."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AmberUpdateCoordinator,
|
||||
description: BinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the Sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.site_id = coordinator.site_id
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.site_id}-{description.key}"
|
||||
self._attr_device_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.coordinator.data["grid"][self.entity_description.key]
|
||||
|
||||
|
||||
class AmberPriceSpikeBinarySensor(AmberPriceGridSensor):
|
||||
"""Sensor to show single grid binary values."""
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the sensor icon."""
|
||||
status = self.coordinator.data["grid"]["price_spike"]
|
||||
return PRICE_SPIKE_ICONS[status]
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.coordinator.data["grid"]["price_spike"] == "spike"
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> Mapping[str, Any] | None:
|
||||
"""Return additional pieces of information about the price spike."""
|
||||
|
||||
spike_status = self.coordinator.data["grid"]["price_spike"]
|
||||
return {
|
||||
"spike_status": spike_status,
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a config entry."""
|
||||
coordinator: AmberUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
entities: list = []
|
||||
price_spike_description = BinarySensorEntityDescription(
|
||||
key="price_spike",
|
||||
name=f"{entry.title} - Price Spike",
|
||||
)
|
||||
entities.append(AmberPriceSpikeBinarySensor(coordinator, price_spike_description))
|
||||
async_add_entities(entities)
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Config flow for the Amber Electric integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import amberelectric
|
||||
from amberelectric.api import amber_api
|
||||
from amberelectric.model.site import Site
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
|
||||
from .const import CONF_SITE_ID, CONF_SITE_NAME, CONF_SITE_NMI, DOMAIN
|
||||
|
||||
API_URL = "https://app.amber.com.au/developers"
|
||||
|
||||
|
||||
class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self._errors: dict[str, str] = {}
|
||||
self._sites: list[Site] | None = None
|
||||
self._api_token: str | None = None
|
||||
|
||||
def _fetch_sites(self, token: str) -> list[Site] | None:
|
||||
configuration = amberelectric.Configuration(access_token=token)
|
||||
api = amber_api.AmberApi.create(configuration)
|
||||
|
||||
try:
|
||||
sites = api.get_sites()
|
||||
if len(sites) == 0:
|
||||
self._errors[CONF_API_TOKEN] = "no_site"
|
||||
return None
|
||||
return sites
|
||||
except amberelectric.ApiException as api_exception:
|
||||
if api_exception.status == 403:
|
||||
self._errors[CONF_API_TOKEN] = "invalid_api_token"
|
||||
else:
|
||||
self._errors[CONF_API_TOKEN] = "unknown_error"
|
||||
return None
|
||||
|
||||
async def async_step_user(self, user_input: dict[str, Any] | None = None):
|
||||
"""Step when user initializes a integration."""
|
||||
self._errors = {}
|
||||
self._sites = None
|
||||
self._api_token = None
|
||||
|
||||
if user_input is not None:
|
||||
token = user_input[CONF_API_TOKEN]
|
||||
self._sites = await self.hass.async_add_executor_job(
|
||||
self._fetch_sites, token
|
||||
)
|
||||
|
||||
if self._sites is not None:
|
||||
self._api_token = token
|
||||
return await self.async_step_site()
|
||||
|
||||
else:
|
||||
user_input = {CONF_API_TOKEN: ""}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
description_placeholders={"api_url": API_URL},
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_API_TOKEN, default=user_input[CONF_API_TOKEN]
|
||||
): str,
|
||||
}
|
||||
),
|
||||
errors=self._errors,
|
||||
)
|
||||
|
||||
async def async_step_site(self, user_input: dict[str, Any] = None):
|
||||
"""Step to select site."""
|
||||
self._errors = {}
|
||||
|
||||
assert self._sites is not None
|
||||
|
||||
api_token = self._api_token
|
||||
if user_input is not None:
|
||||
site_nmi = user_input[CONF_SITE_NMI]
|
||||
sites = [site for site in self._sites if site.nmi == site_nmi]
|
||||
site = sites[0]
|
||||
site_id = site.id
|
||||
name = user_input.get(CONF_SITE_NAME, site_id)
|
||||
return self.async_create_entry(
|
||||
title=name,
|
||||
data={
|
||||
CONF_SITE_ID: site_id,
|
||||
CONF_API_TOKEN: api_token,
|
||||
CONF_SITE_NMI: site.nmi,
|
||||
},
|
||||
)
|
||||
|
||||
user_input = {
|
||||
CONF_API_TOKEN: api_token,
|
||||
CONF_SITE_NMI: "",
|
||||
CONF_SITE_NAME: "",
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="site",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_SITE_NMI, default=user_input[CONF_SITE_NMI]
|
||||
): vol.In([site.nmi for site in self._sites]),
|
||||
vol.Optional(
|
||||
CONF_SITE_NAME, default=user_input[CONF_SITE_NAME]
|
||||
): str,
|
||||
}
|
||||
),
|
||||
errors=self._errors,
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Amber Electric Constants."""
|
||||
import logging
|
||||
|
||||
DOMAIN = "amberelectric"
|
||||
CONF_API_TOKEN = "api_token"
|
||||
CONF_SITE_NAME = "site_name"
|
||||
CONF_SITE_ID = "site_id"
|
||||
CONF_SITE_NMI = "site_nmi"
|
||||
|
||||
ATTRIBUTION = "Data provided by Amber Electric"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
PLATFORMS = ["sensor", "binary_sensor"]
|
||||
@@ -0,0 +1,111 @@
|
||||
"""Amber Electric Coordinator."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from amberelectric import ApiException
|
||||
from amberelectric.api import amber_api
|
||||
from amberelectric.model.actual_interval import ActualInterval
|
||||
from amberelectric.model.channel import ChannelType
|
||||
from amberelectric.model.current_interval import CurrentInterval
|
||||
from amberelectric.model.forecast_interval import ForecastInterval
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import LOGGER
|
||||
|
||||
|
||||
def is_current(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool:
|
||||
"""Return true if the supplied interval is a CurrentInterval."""
|
||||
return isinstance(interval, CurrentInterval)
|
||||
|
||||
|
||||
def is_forecast(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool:
|
||||
"""Return true if the supplied interval is a ForecastInterval."""
|
||||
return isinstance(interval, ForecastInterval)
|
||||
|
||||
|
||||
def is_general(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool:
|
||||
"""Return true if the supplied interval is on the general channel."""
|
||||
return interval.channel_type == ChannelType.GENERAL
|
||||
|
||||
|
||||
def is_controlled_load(
|
||||
interval: ActualInterval | CurrentInterval | ForecastInterval,
|
||||
) -> bool:
|
||||
"""Return true if the supplied interval is on the controlled load channel."""
|
||||
return interval.channel_type == ChannelType.CONTROLLED_LOAD
|
||||
|
||||
|
||||
def is_feed_in(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool:
|
||||
"""Return true if the supplied interval is on the feed in channel."""
|
||||
return interval.channel_type == ChannelType.FEED_IN
|
||||
|
||||
|
||||
class AmberUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""AmberUpdateCoordinator - In charge of downloading the data for a site, which all the sensors read."""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, api: amber_api.AmberApi, site_id: str
|
||||
) -> None:
|
||||
"""Initialise the data service."""
|
||||
super().__init__(
|
||||
hass,
|
||||
LOGGER,
|
||||
name="amberelectric",
|
||||
update_interval=timedelta(minutes=1),
|
||||
)
|
||||
self._api = api
|
||||
self.site_id = site_id
|
||||
|
||||
def update_price_data(self) -> dict[str, dict[str, Any]]:
|
||||
"""Update callback."""
|
||||
|
||||
result: dict[str, dict[str, Any]] = {
|
||||
"current": {},
|
||||
"forecasts": {},
|
||||
"grid": {},
|
||||
}
|
||||
try:
|
||||
data = self._api.get_current_price(self.site_id, next=48)
|
||||
except ApiException as api_exception:
|
||||
raise UpdateFailed("Missing price data, skipping update") from api_exception
|
||||
|
||||
current = [interval for interval in data if is_current(interval)]
|
||||
forecasts = [interval for interval in data if is_forecast(interval)]
|
||||
general = [interval for interval in current if is_general(interval)]
|
||||
|
||||
if len(general) == 0:
|
||||
raise UpdateFailed("No general channel configured")
|
||||
|
||||
result["current"]["general"] = general[0]
|
||||
result["forecasts"]["general"] = [
|
||||
interval for interval in forecasts if is_general(interval)
|
||||
]
|
||||
result["grid"]["renewables"] = round(general[0].renewables)
|
||||
result["grid"]["price_spike"] = general[0].spike_status.value
|
||||
|
||||
controlled_load = [
|
||||
interval for interval in current if is_controlled_load(interval)
|
||||
]
|
||||
if controlled_load:
|
||||
result["current"]["controlled_load"] = controlled_load[0]
|
||||
result["forecasts"]["controlled_load"] = [
|
||||
interval for interval in forecasts if is_controlled_load(interval)
|
||||
]
|
||||
|
||||
feed_in = [interval for interval in current if is_feed_in(interval)]
|
||||
if feed_in:
|
||||
result["current"]["feed_in"] = feed_in[0]
|
||||
result["forecasts"]["feed_in"] = [
|
||||
interval for interval in forecasts if is_feed_in(interval)
|
||||
]
|
||||
|
||||
LOGGER.debug("Fetched new Amber data: %s", data)
|
||||
return result
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Async update wrapper."""
|
||||
return await self.hass.async_add_executor_job(self.update_price_data)
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "amberelectric",
|
||||
"name": "Amber Electric",
|
||||
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
|
||||
"config_flow": true,
|
||||
"codeowners": [
|
||||
"@madpilot"
|
||||
],
|
||||
"requirements": [
|
||||
"amberelectric==1.0.3"
|
||||
],
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
"""Amber Electric Sensor definitions."""
|
||||
|
||||
# There are three types of sensor: Current, Forecast and Grid
|
||||
# Current and forecast will create general, controlled load and feed in as required
|
||||
# At the moment renewables in the only grid sensor.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from amberelectric.model.channel import ChannelType
|
||||
from amberelectric.model.current_interval import CurrentInterval
|
||||
from amberelectric.model.forecast_interval import ForecastInterval
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, CURRENCY_DOLLAR, ENERGY_KILO_WATT_HOUR
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTRIBUTION, DOMAIN
|
||||
from .coordinator import AmberUpdateCoordinator
|
||||
|
||||
ICONS = {
|
||||
"general": "mdi:transmission-tower",
|
||||
"controlled_load": "mdi:clock-outline",
|
||||
"feed_in": "mdi:solar-power",
|
||||
}
|
||||
|
||||
UNIT = f"{CURRENCY_DOLLAR}/{ENERGY_KILO_WATT_HOUR}"
|
||||
|
||||
|
||||
def friendly_channel_type(channel_type: str) -> str:
|
||||
"""Return a human readable version of the channel type."""
|
||||
if channel_type == "controlled_load":
|
||||
return "Controlled Load"
|
||||
if channel_type == "feed_in":
|
||||
return "Feed In"
|
||||
return "General"
|
||||
|
||||
|
||||
class AmberSensor(CoordinatorEntity, SensorEntity):
|
||||
"""Amber Base Sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AmberUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
channel_type: ChannelType,
|
||||
) -> None:
|
||||
"""Initialize the Sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.site_id = coordinator.site_id
|
||||
self.entity_description = description
|
||||
self.channel_type = channel_type
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{self.site_id}-{self.entity_description.key}-{self.channel_type}"
|
||||
)
|
||||
|
||||
|
||||
class AmberPriceSensor(AmberSensor):
|
||||
"""Amber Price Sensor."""
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""Return the current price in $/kWh."""
|
||||
interval = self.coordinator.data[self.entity_description.key][self.channel_type]
|
||||
|
||||
if interval.channel_type == ChannelType.FEED_IN:
|
||||
return round(interval.per_kwh, 0) / 100 * -1
|
||||
return round(interval.per_kwh, 0) / 100
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> Mapping[str, Any] | None:
|
||||
"""Return additional pieces of information about the price."""
|
||||
interval = self.coordinator.data[self.entity_description.key][self.channel_type]
|
||||
|
||||
data: dict[str, Any] = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||
if interval is None:
|
||||
return data
|
||||
|
||||
data["duration"] = interval.duration
|
||||
data["date"] = interval.date.isoformat()
|
||||
data["per_kwh"] = round(interval.per_kwh)
|
||||
if interval.channel_type == ChannelType.FEED_IN:
|
||||
data["per_kwh"] = data["per_kwh"] * -1
|
||||
data["nem_date"] = interval.nem_time.isoformat()
|
||||
data["spot_per_kwh"] = round(interval.spot_per_kwh)
|
||||
data["start_time"] = interval.start_time.isoformat()
|
||||
data["end_time"] = interval.end_time.isoformat()
|
||||
data["renewables"] = round(interval.renewables)
|
||||
data["estimate"] = interval.estimate
|
||||
data["spike_status"] = interval.spike_status.value
|
||||
data["channel_type"] = interval.channel_type.value
|
||||
|
||||
if interval.range is not None:
|
||||
data["range_min"] = interval.range.min
|
||||
data["range_max"] = interval.range.max
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class AmberForecastSensor(AmberSensor):
|
||||
"""Amber Forecast Sensor."""
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""Return the first forecast price in $/kWh."""
|
||||
intervals = self.coordinator.data[self.entity_description.key].get(
|
||||
self.channel_type
|
||||
)
|
||||
if not intervals:
|
||||
return None
|
||||
interval = intervals[0]
|
||||
|
||||
if interval.channel_type == ChannelType.FEED_IN:
|
||||
return round(interval.per_kwh, 0) / 100 * -1
|
||||
return round(interval.per_kwh, 0) / 100
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> Mapping[str, Any] | None:
|
||||
"""Return additional pieces of information about the price."""
|
||||
intervals = self.coordinator.data[self.entity_description.key].get(
|
||||
self.channel_type
|
||||
)
|
||||
|
||||
if not intervals:
|
||||
return None
|
||||
|
||||
data = {
|
||||
"forecasts": [],
|
||||
"channel_type": intervals[0].channel_type.value,
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
}
|
||||
|
||||
for interval in intervals:
|
||||
datum = {}
|
||||
datum["duration"] = interval.duration
|
||||
datum["date"] = interval.date.isoformat()
|
||||
datum["nem_date"] = interval.nem_time.isoformat()
|
||||
datum["per_kwh"] = round(interval.per_kwh)
|
||||
if interval.channel_type == ChannelType.FEED_IN:
|
||||
datum["per_kwh"] = datum["per_kwh"] * -1
|
||||
datum["spot_per_kwh"] = round(interval.spot_per_kwh)
|
||||
datum["start_time"] = interval.start_time.isoformat()
|
||||
datum["end_time"] = interval.end_time.isoformat()
|
||||
datum["renewables"] = round(interval.renewables)
|
||||
datum["spike_status"] = interval.spike_status.value
|
||||
|
||||
if interval.range is not None:
|
||||
datum["range_min"] = interval.range.min
|
||||
datum["range_max"] = interval.range.max
|
||||
|
||||
data["forecasts"].append(datum)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class AmberGridSensor(CoordinatorEntity, SensorEntity):
|
||||
"""Sensor to show single grid specific values."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AmberUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the Sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.site_id = coordinator.site_id
|
||||
self.entity_description = description
|
||||
self._attr_device_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||
self._attr_unique_id = f"{coordinator.site_id}-{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""Return the value of the sensor."""
|
||||
return self.coordinator.data["grid"][self.entity_description.key]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up a config entry."""
|
||||
coordinator: AmberUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
current: dict[str, CurrentInterval] = coordinator.data["current"]
|
||||
forecasts: dict[str, list[ForecastInterval]] = coordinator.data["forecasts"]
|
||||
|
||||
entities: list = []
|
||||
for channel_type in current:
|
||||
description = SensorEntityDescription(
|
||||
key="current",
|
||||
name=f"{entry.title} - {friendly_channel_type(channel_type)} Price",
|
||||
native_unit_of_measurement=UNIT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
icon=ICONS[channel_type],
|
||||
)
|
||||
entities.append(AmberPriceSensor(coordinator, description, channel_type))
|
||||
|
||||
for channel_type in forecasts:
|
||||
description = SensorEntityDescription(
|
||||
key="forecasts",
|
||||
name=f"{entry.title} - {friendly_channel_type(channel_type)} Forecast",
|
||||
native_unit_of_measurement=UNIT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
icon=ICONS[channel_type],
|
||||
)
|
||||
entities.append(AmberForecastSensor(coordinator, description, channel_type))
|
||||
|
||||
renewables_description = SensorEntityDescription(
|
||||
key="renewables",
|
||||
name=f"{entry.title} - Renewables",
|
||||
native_unit_of_measurement="%",
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
icon="mdi:solar-power",
|
||||
)
|
||||
entities.append(AmberGridSensor(coordinator, renewables_description))
|
||||
|
||||
async_add_entities(entities)
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_token": "API Token",
|
||||
"site_id": "Site ID"
|
||||
},
|
||||
"title": "Amber Electric",
|
||||
"description": "Go to {api_url} to generate an API key"
|
||||
},
|
||||
"site": {
|
||||
"data": {
|
||||
"site_nmi": "Site NMI",
|
||||
"site_name": "Site Name"
|
||||
},
|
||||
"title": "Amber Electric",
|
||||
"description": "Select the NMI of the site you would like to add"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"site": {
|
||||
"data": {
|
||||
"site_name": "Site Name",
|
||||
"site_nmi": "Site NMI"
|
||||
},
|
||||
"description": "Select the NMI of the site you would like to add",
|
||||
"title": "Amber Electric"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_token": "API Token",
|
||||
"site_id": "Site ID"
|
||||
},
|
||||
"description": "Go to {api_url} to generate an API key",
|
||||
"title": "Amber Electric"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,6 +142,7 @@ class AmbiclimateAuthCallbackView(HomeAssistantView):
|
||||
|
||||
async def get(self, request: web.Request) -> str:
|
||||
"""Receive authorization token."""
|
||||
# pylint: disable=no-self-use
|
||||
code = request.query.get("code")
|
||||
if code is None:
|
||||
return "No code"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Authentifi\u00e9 avec succ\u00e8s avec Ambiclimate"
|
||||
"default": "Authentification r\u00e9ussie"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.",
|
||||
|
||||
@@ -1,38 +1,17 @@
|
||||
"""Support for Ambient Weather Station Service."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aioambient import Client
|
||||
from aioambient.errors import WebsocketError
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASS_CONNECTIVITY,
|
||||
DOMAIN as BINARY_SENSOR,
|
||||
)
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_LOCATION,
|
||||
ATTR_NAME,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
CONF_API_KEY,
|
||||
DEGREE,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_CO2,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
IRRADIATION_WATTS_PER_SQUARE_METER,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
PRECIPITATION_INCHES,
|
||||
PRECIPITATION_INCHES_PER_HOUR,
|
||||
PRESSURE_INHG,
|
||||
SPEED_MILES_PER_HOUR,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
@@ -41,266 +20,43 @@ from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from .const import (
|
||||
ATTR_LAST_DATA,
|
||||
ATTR_MONITORED_CONDITIONS,
|
||||
CONF_APP_KEY,
|
||||
DATA_CLIENT,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
TYPE_SOLARRADIATION,
|
||||
TYPE_SOLARRADIATION_LX,
|
||||
)
|
||||
|
||||
PLATFORMS = [BINARY_SENSOR, SENSOR]
|
||||
PLATFORMS = ["binary_sensor", "sensor"]
|
||||
|
||||
DATA_CONFIG = "config"
|
||||
|
||||
DEFAULT_SOCKET_MIN_RETRY = 15
|
||||
|
||||
TYPE_24HOURRAININ = "24hourrainin"
|
||||
TYPE_BAROMABSIN = "baromabsin"
|
||||
TYPE_BAROMRELIN = "baromrelin"
|
||||
TYPE_BATT1 = "batt1"
|
||||
TYPE_BATT10 = "batt10"
|
||||
TYPE_BATT2 = "batt2"
|
||||
TYPE_BATT3 = "batt3"
|
||||
TYPE_BATT4 = "batt4"
|
||||
TYPE_BATT5 = "batt5"
|
||||
TYPE_BATT6 = "batt6"
|
||||
TYPE_BATT7 = "batt7"
|
||||
TYPE_BATT8 = "batt8"
|
||||
TYPE_BATT9 = "batt9"
|
||||
TYPE_BATT_CO2 = "batt_co2"
|
||||
TYPE_BATTOUT = "battout"
|
||||
TYPE_CO2 = "co2"
|
||||
TYPE_DAILYRAININ = "dailyrainin"
|
||||
TYPE_DEWPOINT = "dewPoint"
|
||||
TYPE_EVENTRAININ = "eventrainin"
|
||||
TYPE_FEELSLIKE = "feelsLike"
|
||||
TYPE_HOURLYRAININ = "hourlyrainin"
|
||||
TYPE_HUMIDITY = "humidity"
|
||||
TYPE_HUMIDITY1 = "humidity1"
|
||||
TYPE_HUMIDITY10 = "humidity10"
|
||||
TYPE_HUMIDITY2 = "humidity2"
|
||||
TYPE_HUMIDITY3 = "humidity3"
|
||||
TYPE_HUMIDITY4 = "humidity4"
|
||||
TYPE_HUMIDITY5 = "humidity5"
|
||||
TYPE_HUMIDITY6 = "humidity6"
|
||||
TYPE_HUMIDITY7 = "humidity7"
|
||||
TYPE_HUMIDITY8 = "humidity8"
|
||||
TYPE_HUMIDITY9 = "humidity9"
|
||||
TYPE_HUMIDITYIN = "humidityin"
|
||||
TYPE_LASTRAIN = "lastRain"
|
||||
TYPE_MAXDAILYGUST = "maxdailygust"
|
||||
TYPE_MONTHLYRAININ = "monthlyrainin"
|
||||
TYPE_PM25 = "pm25"
|
||||
TYPE_PM25_24H = "pm25_24h"
|
||||
TYPE_PM25_BATT = "batt_25"
|
||||
TYPE_PM25_IN = "pm25_in"
|
||||
TYPE_PM25_IN_24H = "pm25_in_24h"
|
||||
TYPE_PM25IN_BATT = "batt_25in"
|
||||
TYPE_RELAY1 = "relay1"
|
||||
TYPE_RELAY10 = "relay10"
|
||||
TYPE_RELAY2 = "relay2"
|
||||
TYPE_RELAY3 = "relay3"
|
||||
TYPE_RELAY4 = "relay4"
|
||||
TYPE_RELAY5 = "relay5"
|
||||
TYPE_RELAY6 = "relay6"
|
||||
TYPE_RELAY7 = "relay7"
|
||||
TYPE_RELAY8 = "relay8"
|
||||
TYPE_RELAY9 = "relay9"
|
||||
TYPE_SOILHUM1 = "soilhum1"
|
||||
TYPE_SOILHUM10 = "soilhum10"
|
||||
TYPE_SOILHUM2 = "soilhum2"
|
||||
TYPE_SOILHUM3 = "soilhum3"
|
||||
TYPE_SOILHUM4 = "soilhum4"
|
||||
TYPE_SOILHUM5 = "soilhum5"
|
||||
TYPE_SOILHUM6 = "soilhum6"
|
||||
TYPE_SOILHUM7 = "soilhum7"
|
||||
TYPE_SOILHUM8 = "soilhum8"
|
||||
TYPE_SOILHUM9 = "soilhum9"
|
||||
TYPE_SOILTEMP1F = "soiltemp1f"
|
||||
TYPE_SOILTEMP10F = "soiltemp10f"
|
||||
TYPE_SOILTEMP2F = "soiltemp2f"
|
||||
TYPE_SOILTEMP3F = "soiltemp3f"
|
||||
TYPE_SOILTEMP4F = "soiltemp4f"
|
||||
TYPE_SOILTEMP5F = "soiltemp5f"
|
||||
TYPE_SOILTEMP6F = "soiltemp6f"
|
||||
TYPE_SOILTEMP7F = "soiltemp7f"
|
||||
TYPE_SOILTEMP8F = "soiltemp8f"
|
||||
TYPE_SOILTEMP9F = "soiltemp9f"
|
||||
TYPE_SOLARRADIATION = "solarradiation"
|
||||
TYPE_SOLARRADIATION_LX = "solarradiation_lx"
|
||||
TYPE_TEMP10F = "temp10f"
|
||||
TYPE_TEMP1F = "temp1f"
|
||||
TYPE_TEMP2F = "temp2f"
|
||||
TYPE_TEMP3F = "temp3f"
|
||||
TYPE_TEMP4F = "temp4f"
|
||||
TYPE_TEMP5F = "temp5f"
|
||||
TYPE_TEMP6F = "temp6f"
|
||||
TYPE_TEMP7F = "temp7f"
|
||||
TYPE_TEMP8F = "temp8f"
|
||||
TYPE_TEMP9F = "temp9f"
|
||||
TYPE_TEMPF = "tempf"
|
||||
TYPE_TEMPINF = "tempinf"
|
||||
TYPE_TOTALRAININ = "totalrainin"
|
||||
TYPE_UV = "uv"
|
||||
TYPE_WEEKLYRAININ = "weeklyrainin"
|
||||
TYPE_WINDDIR = "winddir"
|
||||
TYPE_WINDDIR_AVG10M = "winddir_avg10m"
|
||||
TYPE_WINDDIR_AVG2M = "winddir_avg2m"
|
||||
TYPE_WINDGUSTDIR = "windgustdir"
|
||||
TYPE_WINDGUSTMPH = "windgustmph"
|
||||
TYPE_WINDSPDMPH_AVG10M = "windspdmph_avg10m"
|
||||
TYPE_WINDSPDMPH_AVG2M = "windspdmph_avg2m"
|
||||
TYPE_WINDSPEEDMPH = "windspeedmph"
|
||||
TYPE_YEARLYRAININ = "yearlyrainin"
|
||||
SENSOR_TYPES = {
|
||||
TYPE_24HOURRAININ: ("24 Hr Rain", PRECIPITATION_INCHES, SENSOR, None),
|
||||
TYPE_BAROMABSIN: ("Abs Pressure", PRESSURE_INHG, SENSOR, DEVICE_CLASS_PRESSURE),
|
||||
TYPE_BAROMRELIN: ("Rel Pressure", PRESSURE_INHG, SENSOR, DEVICE_CLASS_PRESSURE),
|
||||
TYPE_BATT10: ("Battery 10", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
|
||||
TYPE_BATT1: ("Battery 1", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
|
||||
TYPE_BATT2: ("Battery 2", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
|
||||
TYPE_BATT3: ("Battery 3", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
|
||||
TYPE_BATT4: ("Battery 4", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
|
||||
TYPE_BATT5: ("Battery 5", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
|
||||
TYPE_BATT6: ("Battery 6", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
|
||||
TYPE_BATT7: ("Battery 7", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
|
||||
TYPE_BATT8: ("Battery 8", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
|
||||
TYPE_BATT9: ("Battery 9", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
|
||||
TYPE_BATTOUT: ("Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
|
||||
TYPE_BATT_CO2: ("CO2 Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
|
||||
TYPE_CO2: ("co2", CONCENTRATION_PARTS_PER_MILLION, SENSOR, DEVICE_CLASS_CO2),
|
||||
TYPE_DAILYRAININ: ("Daily Rain", PRECIPITATION_INCHES, SENSOR, None),
|
||||
TYPE_DEWPOINT: ("Dew Point", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_EVENTRAININ: ("Event Rain", PRECIPITATION_INCHES, SENSOR, None),
|
||||
TYPE_FEELSLIKE: ("Feels Like", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_HOURLYRAININ: (
|
||||
"Hourly Rain Rate",
|
||||
PRECIPITATION_INCHES_PER_HOUR,
|
||||
SENSOR,
|
||||
None,
|
||||
),
|
||||
TYPE_HUMIDITY10: ("Humidity 10", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_HUMIDITY1: ("Humidity 1", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_HUMIDITY2: ("Humidity 2", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_HUMIDITY3: ("Humidity 3", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_HUMIDITY4: ("Humidity 4", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_HUMIDITY5: ("Humidity 5", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_HUMIDITY6: ("Humidity 6", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_HUMIDITY7: ("Humidity 7", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_HUMIDITY8: ("Humidity 8", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_HUMIDITY9: ("Humidity 9", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_HUMIDITY: ("Humidity", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_HUMIDITYIN: ("Humidity In", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_LASTRAIN: ("Last Rain", None, SENSOR, DEVICE_CLASS_TIMESTAMP),
|
||||
TYPE_MAXDAILYGUST: ("Max Gust", SPEED_MILES_PER_HOUR, SENSOR, None),
|
||||
TYPE_MONTHLYRAININ: ("Monthly Rain", PRECIPITATION_INCHES, SENSOR, None),
|
||||
TYPE_PM25_24H: (
|
||||
"PM25 24h Avg",
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
SENSOR,
|
||||
None,
|
||||
),
|
||||
TYPE_PM25_BATT: ("PM25 Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
|
||||
TYPE_PM25_IN: (
|
||||
"PM25 Indoor",
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
SENSOR,
|
||||
None,
|
||||
),
|
||||
TYPE_PM25_IN_24H: (
|
||||
"PM25 Indoor 24h Avg",
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
SENSOR,
|
||||
None,
|
||||
),
|
||||
TYPE_PM25: ("PM25", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, SENSOR, None),
|
||||
TYPE_PM25IN_BATT: (
|
||||
"PM25 Indoor Battery",
|
||||
None,
|
||||
BINARY_SENSOR,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
),
|
||||
TYPE_RELAY10: ("Relay 10", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
|
||||
TYPE_RELAY1: ("Relay 1", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
|
||||
TYPE_RELAY2: ("Relay 2", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
|
||||
TYPE_RELAY3: ("Relay 3", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
|
||||
TYPE_RELAY4: ("Relay 4", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
|
||||
TYPE_RELAY5: ("Relay 5", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
|
||||
TYPE_RELAY6: ("Relay 6", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
|
||||
TYPE_RELAY7: ("Relay 7", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
|
||||
TYPE_RELAY8: ("Relay 8", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
|
||||
TYPE_RELAY9: ("Relay 9", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
|
||||
TYPE_SOILHUM10: ("Soil Humidity 10", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_SOILHUM1: ("Soil Humidity 1", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_SOILHUM2: ("Soil Humidity 2", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_SOILHUM3: ("Soil Humidity 3", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_SOILHUM4: ("Soil Humidity 4", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_SOILHUM5: ("Soil Humidity 5", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_SOILHUM6: ("Soil Humidity 6", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_SOILHUM7: ("Soil Humidity 7", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_SOILHUM8: ("Soil Humidity 8", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_SOILHUM9: ("Soil Humidity 9", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_SOILTEMP10F: (
|
||||
"Soil Temp 10",
|
||||
TEMP_FAHRENHEIT,
|
||||
SENSOR,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
),
|
||||
TYPE_SOILTEMP1F: ("Soil Temp 1", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_SOILTEMP2F: ("Soil Temp 2", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_SOILTEMP3F: ("Soil Temp 3", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_SOILTEMP4F: ("Soil Temp 4", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_SOILTEMP5F: ("Soil Temp 5", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_SOILTEMP6F: ("Soil Temp 6", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_SOILTEMP7F: ("Soil Temp 7", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_SOILTEMP8F: ("Soil Temp 8", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_SOILTEMP9F: ("Soil Temp 9", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_SOLARRADIATION: (
|
||||
"Solar Rad",
|
||||
IRRADIATION_WATTS_PER_SQUARE_METER,
|
||||
SENSOR,
|
||||
None,
|
||||
),
|
||||
TYPE_SOLARRADIATION_LX: (
|
||||
"Solar Rad (lx)",
|
||||
LIGHT_LUX,
|
||||
SENSOR,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
),
|
||||
TYPE_TEMP10F: ("Temp 10", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_TEMP1F: ("Temp 1", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_TEMP2F: ("Temp 2", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_TEMP3F: ("Temp 3", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_TEMP4F: ("Temp 4", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_TEMP5F: ("Temp 5", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_TEMP6F: ("Temp 6", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_TEMP7F: ("Temp 7", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_TEMP8F: ("Temp 8", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_TEMP9F: ("Temp 9", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_TEMPF: ("Temp", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_TEMPINF: ("Inside Temp", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_TOTALRAININ: ("Lifetime Rain", PRECIPITATION_INCHES, SENSOR, None),
|
||||
TYPE_UV: ("uv", "Index", SENSOR, None),
|
||||
TYPE_WEEKLYRAININ: ("Weekly Rain", PRECIPITATION_INCHES, SENSOR, None),
|
||||
TYPE_WINDDIR: ("Wind Dir", DEGREE, SENSOR, None),
|
||||
TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", DEGREE, SENSOR, None),
|
||||
TYPE_WINDDIR_AVG2M: ("Wind Dir Avg 2m", SPEED_MILES_PER_HOUR, SENSOR, None),
|
||||
TYPE_WINDGUSTDIR: ("Gust Dir", DEGREE, SENSOR, None),
|
||||
TYPE_WINDGUSTMPH: ("Wind Gust", SPEED_MILES_PER_HOUR, SENSOR, None),
|
||||
TYPE_WINDSPDMPH_AVG10M: ("Wind Avg 10m", SPEED_MILES_PER_HOUR, SENSOR, None),
|
||||
TYPE_WINDSPDMPH_AVG2M: ("Wind Avg 2m", SPEED_MILES_PER_HOUR, SENSOR, None),
|
||||
TYPE_WINDSPEEDMPH: ("Wind Speed", SPEED_MILES_PER_HOUR, SENSOR, None),
|
||||
TYPE_YEARLYRAININ: ("Yearly Rain", PRECIPITATION_INCHES, SENSOR, None),
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
|
||||
|
||||
|
||||
@callback
|
||||
def async_wm2_to_lx(value: float) -> int:
|
||||
"""Calculate illuminance (in lux)."""
|
||||
return round(value / 0.0079)
|
||||
|
||||
|
||||
@callback
|
||||
def async_hydrate_station_data(data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Hydrate station data with addition or normalized data."""
|
||||
if (irradiation := data.get(TYPE_SOLARRADIATION)) is not None:
|
||||
data[TYPE_SOLARRADIATION_LX] = async_wm2_to_lx(irradiation)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up the Ambient PWS as config entry."""
|
||||
hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}})
|
||||
@@ -319,6 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
config_entry.data[CONF_API_KEY],
|
||||
config_entry.data[CONF_APP_KEY],
|
||||
session=session,
|
||||
logger=LOGGER,
|
||||
),
|
||||
)
|
||||
hass.loop.create_task(ambient.ws_connect())
|
||||
@@ -405,13 +162,14 @@ class AmbientStation:
|
||||
|
||||
def on_data(data: dict) -> None:
|
||||
"""Define a handler to fire when the data is received."""
|
||||
mac_address = data["macAddress"]
|
||||
if data != self.stations[mac_address][ATTR_LAST_DATA]:
|
||||
LOGGER.debug("New data received: %s", data)
|
||||
self.stations[mac_address][ATTR_LAST_DATA] = data
|
||||
async_dispatcher_send(
|
||||
self._hass, f"ambient_station_data_update_{mac_address}"
|
||||
)
|
||||
mac = data["macAddress"]
|
||||
|
||||
if data == self.stations[mac][ATTR_LAST_DATA]:
|
||||
return
|
||||
|
||||
LOGGER.debug("New data received: %s", data)
|
||||
self.stations[mac][ATTR_LAST_DATA] = async_hydrate_station_data(data)
|
||||
async_dispatcher_send(self._hass, f"ambient_station_data_update_{mac}")
|
||||
|
||||
def on_disconnect() -> None:
|
||||
"""Define a handler to fire when the websocket is disconnected."""
|
||||
@@ -420,26 +178,17 @@ class AmbientStation:
|
||||
def on_subscribed(data: dict) -> None:
|
||||
"""Define a handler to fire when the subscription is set."""
|
||||
for station in data["devices"]:
|
||||
if station["macAddress"] in self.stations:
|
||||
if (mac := station["macAddress"]) in self.stations:
|
||||
continue
|
||||
|
||||
LOGGER.debug("New station subscription: %s", data)
|
||||
|
||||
# Only create entities based on the data coming through the socket.
|
||||
# If the user is monitoring brightness (in W/m^2), make sure we also
|
||||
# add a calculated sensor for the same data measured in lx:
|
||||
monitored_conditions = [
|
||||
k for k in station["lastData"] if k in SENSOR_TYPES
|
||||
]
|
||||
if TYPE_SOLARRADIATION in monitored_conditions:
|
||||
monitored_conditions.append(TYPE_SOLARRADIATION_LX)
|
||||
self.stations[station["macAddress"]] = {
|
||||
ATTR_LAST_DATA: station["lastData"],
|
||||
self.stations[mac] = {
|
||||
ATTR_LAST_DATA: async_hydrate_station_data(station["lastData"]),
|
||||
ATTR_LOCATION: station.get("info", {}).get("location"),
|
||||
ATTR_MONITORED_CONDITIONS: monitored_conditions,
|
||||
ATTR_NAME: station.get("info", {}).get(
|
||||
"name", station["macAddress"]
|
||||
),
|
||||
ATTR_NAME: station.get("info", {}).get("name", mac),
|
||||
}
|
||||
|
||||
# If the websocket disconnects and reconnects, the on_subscribed
|
||||
# handler will get called again; in that case, we don't want to
|
||||
# attempt forward setup of the config entry (because it will have
|
||||
@@ -466,28 +215,26 @@ class AmbientStation:
|
||||
class AmbientWeatherEntity(Entity):
|
||||
"""Define a base Ambient PWS entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ambient: AmbientStation,
|
||||
mac_address: str,
|
||||
station_name: str,
|
||||
sensor_type: str,
|
||||
sensor_name: str,
|
||||
device_class: str | None,
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
"""Initialize the entity."""
|
||||
self._ambient = ambient
|
||||
self._attr_device_class = device_class
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, mac_address)},
|
||||
"name": station_name,
|
||||
"manufacturer": "Ambient Weather",
|
||||
}
|
||||
self._attr_name = f"{station_name}_{sensor_name}"
|
||||
self._attr_should_poll = False
|
||||
self._attr_unique_id = f"{mac_address}_{sensor_type}"
|
||||
self._attr_name = f"{station_name}_{description.name}"
|
||||
self._attr_unique_id = f"{mac_address}_{description.key}"
|
||||
self._mac_address = mac_address
|
||||
self._sensor_type = sensor_type
|
||||
self.entity_description = description
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
@@ -495,18 +242,18 @@ class AmbientWeatherEntity(Entity):
|
||||
@callback
|
||||
def update() -> None:
|
||||
"""Update the state."""
|
||||
if self._sensor_type == TYPE_SOLARRADIATION_LX:
|
||||
if self.entity_description.key == TYPE_SOLARRADIATION_LX:
|
||||
self._attr_available = (
|
||||
self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get(
|
||||
self._ambient.stations[self._mac_address][ATTR_LAST_DATA][
|
||||
TYPE_SOLARRADIATION
|
||||
)
|
||||
]
|
||||
is not None
|
||||
)
|
||||
else:
|
||||
self._attr_available = (
|
||||
self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get(
|
||||
self._sensor_type
|
||||
)
|
||||
self._ambient.stations[self._mac_address][ATTR_LAST_DATA][
|
||||
self.entity_description.key
|
||||
]
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
@@ -1,34 +1,209 @@
|
||||
"""Support for Ambient Weather Station binary sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_CONNECTIVITY,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import (
|
||||
SENSOR_TYPES,
|
||||
TYPE_BATT1,
|
||||
TYPE_BATT2,
|
||||
TYPE_BATT3,
|
||||
TYPE_BATT4,
|
||||
TYPE_BATT5,
|
||||
TYPE_BATT6,
|
||||
TYPE_BATT7,
|
||||
TYPE_BATT8,
|
||||
TYPE_BATT9,
|
||||
TYPE_BATT10,
|
||||
TYPE_BATT_CO2,
|
||||
TYPE_BATTOUT,
|
||||
TYPE_PM25_BATT,
|
||||
TYPE_PM25IN_BATT,
|
||||
AmbientWeatherEntity,
|
||||
from . import AmbientWeatherEntity
|
||||
from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN
|
||||
|
||||
TYPE_BATT1 = "batt1"
|
||||
TYPE_BATT10 = "batt10"
|
||||
TYPE_BATT2 = "batt2"
|
||||
TYPE_BATT3 = "batt3"
|
||||
TYPE_BATT4 = "batt4"
|
||||
TYPE_BATT5 = "batt5"
|
||||
TYPE_BATT6 = "batt6"
|
||||
TYPE_BATT7 = "batt7"
|
||||
TYPE_BATT8 = "batt8"
|
||||
TYPE_BATT9 = "batt9"
|
||||
TYPE_BATT_CO2 = "batt_co2"
|
||||
TYPE_BATTOUT = "battout"
|
||||
TYPE_PM25_BATT = "batt_25"
|
||||
TYPE_PM25IN_BATT = "batt_25in"
|
||||
TYPE_RELAY1 = "relay1"
|
||||
TYPE_RELAY10 = "relay10"
|
||||
TYPE_RELAY2 = "relay2"
|
||||
TYPE_RELAY3 = "relay3"
|
||||
TYPE_RELAY4 = "relay4"
|
||||
TYPE_RELAY5 = "relay5"
|
||||
TYPE_RELAY6 = "relay6"
|
||||
TYPE_RELAY7 = "relay7"
|
||||
TYPE_RELAY8 = "relay8"
|
||||
TYPE_RELAY9 = "relay9"
|
||||
|
||||
|
||||
@dataclass
|
||||
class AmbientBinarySensorDescriptionMixin:
|
||||
"""Define an entity description mixin for binary sensors."""
|
||||
|
||||
on_state: Literal[0, 1]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AmbientBinarySensorDescription(
|
||||
BinarySensorEntityDescription, AmbientBinarySensorDescriptionMixin
|
||||
):
|
||||
"""Describe an Ambient PWS binary sensor."""
|
||||
|
||||
|
||||
BINARY_SENSOR_DESCRIPTIONS = (
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATTOUT,
|
||||
name="Battery",
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT1,
|
||||
name="Battery 1",
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT2,
|
||||
name="Battery 2",
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT3,
|
||||
name="Battery 3",
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT4,
|
||||
name="Battery 4",
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT5,
|
||||
name="Battery 5",
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT6,
|
||||
name="Battery 6",
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT7,
|
||||
name="Battery 7",
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT8,
|
||||
name="Battery 8",
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT9,
|
||||
name="Battery 9",
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT10,
|
||||
name="Battery 10",
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_CO2,
|
||||
name="CO2 Battery",
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_PM25IN_BATT,
|
||||
name="PM25 Indoor Battery",
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_PM25_BATT,
|
||||
name="PM25 Battery",
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY1,
|
||||
name="Relay 1",
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY2,
|
||||
name="Relay 2",
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY3,
|
||||
name="Relay 3",
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY4,
|
||||
name="Relay 4",
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY5,
|
||||
name="Relay 5",
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY6,
|
||||
name="Relay 6",
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY7,
|
||||
name="Relay 7",
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY8,
|
||||
name="Relay 8",
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY9,
|
||||
name="Relay 9",
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_RELAY10,
|
||||
name="Relay 10",
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
on_state=1,
|
||||
),
|
||||
)
|
||||
from .const import ATTR_LAST_DATA, ATTR_MONITORED_CONDITIONS, DATA_CLIENT, DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -37,51 +212,29 @@ async def async_setup_entry(
|
||||
"""Set up Ambient PWS binary sensors based on a config entry."""
|
||||
ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
|
||||
|
||||
binary_sensor_list = []
|
||||
for mac_address, station in ambient.stations.items():
|
||||
for condition in station[ATTR_MONITORED_CONDITIONS]:
|
||||
name, _, kind, device_class = SENSOR_TYPES[condition]
|
||||
if kind == BINARY_SENSOR:
|
||||
binary_sensor_list.append(
|
||||
AmbientWeatherBinarySensor(
|
||||
ambient,
|
||||
mac_address,
|
||||
station[ATTR_NAME],
|
||||
condition,
|
||||
name,
|
||||
device_class,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(binary_sensor_list)
|
||||
async_add_entities(
|
||||
[
|
||||
AmbientWeatherBinarySensor(
|
||||
ambient, mac_address, station[ATTR_NAME], description
|
||||
)
|
||||
for mac_address, station in ambient.stations.items()
|
||||
for description in BINARY_SENSOR_DESCRIPTIONS
|
||||
if description.key in station[ATTR_LAST_DATA]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorEntity):
|
||||
"""Define an Ambient binary sensor."""
|
||||
|
||||
entity_description: AmbientBinarySensorDescription
|
||||
|
||||
@callback
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Fetch new state data for the entity."""
|
||||
state = self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get(
|
||||
self._sensor_type
|
||||
self._attr_is_on = (
|
||||
self._ambient.stations[self._mac_address][ATTR_LAST_DATA][
|
||||
self.entity_description.key
|
||||
]
|
||||
== self.entity_description.on_state
|
||||
)
|
||||
|
||||
if self._sensor_type in (
|
||||
TYPE_BATT1,
|
||||
TYPE_BATT10,
|
||||
TYPE_BATT2,
|
||||
TYPE_BATT3,
|
||||
TYPE_BATT4,
|
||||
TYPE_BATT5,
|
||||
TYPE_BATT6,
|
||||
TYPE_BATT7,
|
||||
TYPE_BATT8,
|
||||
TYPE_BATT9,
|
||||
TYPE_BATT_CO2,
|
||||
TYPE_BATTOUT,
|
||||
TYPE_PM25_BATT,
|
||||
TYPE_PM25IN_BATT,
|
||||
):
|
||||
self._attr_is_on = state == 0
|
||||
else:
|
||||
self._attr_is_on = state == 1
|
||||
|
||||
@@ -5,8 +5,10 @@ DOMAIN = "ambient_station"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
ATTR_LAST_DATA = "last_data"
|
||||
ATTR_MONITORED_CONDITIONS = "monitored_conditions"
|
||||
|
||||
CONF_APP_KEY = "app_key"
|
||||
|
||||
DATA_CLIENT = "data_client"
|
||||
|
||||
TYPE_SOLARRADIATION = "solarradiation"
|
||||
TYPE_SOLARRADIATION_LX = "solarradiation_lx"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Ambient Weather Station",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ambient_station",
|
||||
"requirements": ["aioambient==1.2.6"],
|
||||
"requirements": ["aioambient==1.3.0"],
|
||||
"codeowners": ["@bachya"],
|
||||
"iot_class": "cloud_push"
|
||||
}
|
||||
|
||||
@@ -1,20 +1,608 @@
|
||||
"""Support for Ambient Weather Station sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity
|
||||
from homeassistant.components.sensor import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.const import (
|
||||
ATTR_NAME,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
DEGREE,
|
||||
DEVICE_CLASS_CO2,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
DEVICE_CLASS_PM25,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
IRRADIATION_WATTS_PER_SQUARE_METER,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
PRECIPITATION_INCHES,
|
||||
PRECIPITATION_INCHES_PER_HOUR,
|
||||
PRESSURE_INHG,
|
||||
SPEED_MILES_PER_HOUR,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import (
|
||||
SENSOR_TYPES,
|
||||
TYPE_SOLARRADIATION,
|
||||
TYPE_SOLARRADIATION_LX,
|
||||
AmbientStation,
|
||||
AmbientWeatherEntity,
|
||||
)
|
||||
from .const import ATTR_LAST_DATA, ATTR_MONITORED_CONDITIONS, DATA_CLIENT, DOMAIN
|
||||
from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN
|
||||
|
||||
TYPE_24HOURRAININ = "24hourrainin"
|
||||
TYPE_BAROMABSIN = "baromabsin"
|
||||
TYPE_BAROMRELIN = "baromrelin"
|
||||
TYPE_CO2 = "co2"
|
||||
TYPE_DAILYRAININ = "dailyrainin"
|
||||
TYPE_DEWPOINT = "dewPoint"
|
||||
TYPE_EVENTRAININ = "eventrainin"
|
||||
TYPE_FEELSLIKE = "feelsLike"
|
||||
TYPE_HOURLYRAININ = "hourlyrainin"
|
||||
TYPE_HUMIDITY = "humidity"
|
||||
TYPE_HUMIDITY1 = "humidity1"
|
||||
TYPE_HUMIDITY10 = "humidity10"
|
||||
TYPE_HUMIDITY2 = "humidity2"
|
||||
TYPE_HUMIDITY3 = "humidity3"
|
||||
TYPE_HUMIDITY4 = "humidity4"
|
||||
TYPE_HUMIDITY5 = "humidity5"
|
||||
TYPE_HUMIDITY6 = "humidity6"
|
||||
TYPE_HUMIDITY7 = "humidity7"
|
||||
TYPE_HUMIDITY8 = "humidity8"
|
||||
TYPE_HUMIDITY9 = "humidity9"
|
||||
TYPE_HUMIDITYIN = "humidityin"
|
||||
TYPE_LASTRAIN = "lastRain"
|
||||
TYPE_MAXDAILYGUST = "maxdailygust"
|
||||
TYPE_MONTHLYRAININ = "monthlyrainin"
|
||||
TYPE_PM25 = "pm25"
|
||||
TYPE_PM25_24H = "pm25_24h"
|
||||
TYPE_PM25_IN = "pm25_in"
|
||||
TYPE_PM25_IN_24H = "pm25_in_24h"
|
||||
TYPE_SOILHUM1 = "soilhum1"
|
||||
TYPE_SOILHUM10 = "soilhum10"
|
||||
TYPE_SOILHUM2 = "soilhum2"
|
||||
TYPE_SOILHUM3 = "soilhum3"
|
||||
TYPE_SOILHUM4 = "soilhum4"
|
||||
TYPE_SOILHUM5 = "soilhum5"
|
||||
TYPE_SOILHUM6 = "soilhum6"
|
||||
TYPE_SOILHUM7 = "soilhum7"
|
||||
TYPE_SOILHUM8 = "soilhum8"
|
||||
TYPE_SOILHUM9 = "soilhum9"
|
||||
TYPE_SOILTEMP1F = "soiltemp1f"
|
||||
TYPE_SOILTEMP10F = "soiltemp10f"
|
||||
TYPE_SOILTEMP2F = "soiltemp2f"
|
||||
TYPE_SOILTEMP3F = "soiltemp3f"
|
||||
TYPE_SOILTEMP4F = "soiltemp4f"
|
||||
TYPE_SOILTEMP5F = "soiltemp5f"
|
||||
TYPE_SOILTEMP6F = "soiltemp6f"
|
||||
TYPE_SOILTEMP7F = "soiltemp7f"
|
||||
TYPE_SOILTEMP8F = "soiltemp8f"
|
||||
TYPE_SOILTEMP9F = "soiltemp9f"
|
||||
TYPE_TEMP10F = "temp10f"
|
||||
TYPE_TEMP1F = "temp1f"
|
||||
TYPE_TEMP2F = "temp2f"
|
||||
TYPE_TEMP3F = "temp3f"
|
||||
TYPE_TEMP4F = "temp4f"
|
||||
TYPE_TEMP5F = "temp5f"
|
||||
TYPE_TEMP6F = "temp6f"
|
||||
TYPE_TEMP7F = "temp7f"
|
||||
TYPE_TEMP8F = "temp8f"
|
||||
TYPE_TEMP9F = "temp9f"
|
||||
TYPE_TEMPF = "tempf"
|
||||
TYPE_TEMPINF = "tempinf"
|
||||
TYPE_TOTALRAININ = "totalrainin"
|
||||
TYPE_UV = "uv"
|
||||
TYPE_WEEKLYRAININ = "weeklyrainin"
|
||||
TYPE_WINDDIR = "winddir"
|
||||
TYPE_WINDDIR_AVG10M = "winddir_avg10m"
|
||||
TYPE_WINDDIR_AVG2M = "winddir_avg2m"
|
||||
TYPE_WINDGUSTDIR = "windgustdir"
|
||||
TYPE_WINDGUSTMPH = "windgustmph"
|
||||
TYPE_WINDSPDMPH_AVG10M = "windspdmph_avg10m"
|
||||
TYPE_WINDSPDMPH_AVG2M = "windspdmph_avg2m"
|
||||
TYPE_WINDSPEEDMPH = "windspeedmph"
|
||||
TYPE_YEARLYRAININ = "yearlyrainin"
|
||||
|
||||
SENSOR_DESCRIPTIONS = (
|
||||
SensorEntityDescription(
|
||||
key=TYPE_24HOURRAININ,
|
||||
name="24 Hr Rain",
|
||||
icon="mdi:water",
|
||||
native_unit_of_measurement=PRECIPITATION_INCHES,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_BAROMABSIN,
|
||||
name="Abs Pressure",
|
||||
native_unit_of_measurement=PRESSURE_INHG,
|
||||
device_class=DEVICE_CLASS_PRESSURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_BAROMRELIN,
|
||||
name="Rel Pressure",
|
||||
native_unit_of_measurement=PRESSURE_INHG,
|
||||
device_class=DEVICE_CLASS_PRESSURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_CO2,
|
||||
name="co2",
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
device_class=DEVICE_CLASS_CO2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_DAILYRAININ,
|
||||
name="Daily Rain",
|
||||
icon="mdi:water",
|
||||
native_unit_of_measurement=PRECIPITATION_INCHES,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_DEWPOINT,
|
||||
name="Dew Point",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_EVENTRAININ,
|
||||
name="Event Rain",
|
||||
icon="mdi:water",
|
||||
native_unit_of_measurement=PRECIPITATION_INCHES,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_FEELSLIKE,
|
||||
name="Feels Like",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HOURLYRAININ,
|
||||
name="Hourly Rain Rate",
|
||||
icon="mdi:water",
|
||||
native_unit_of_measurement=PRECIPITATION_INCHES_PER_HOUR,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY10,
|
||||
name="Humidity 10",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY1,
|
||||
name="Humidity 1",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY2,
|
||||
name="Humidity 2",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY3,
|
||||
name="Humidity 3",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY4,
|
||||
name="Humidity 4",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY5,
|
||||
name="Humidity 5",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY6,
|
||||
name="Humidity 6",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY7,
|
||||
name="Humidity 7",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY8,
|
||||
name="Humidity 8",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY9,
|
||||
name="Humidity 9",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITY,
|
||||
name="Humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_HUMIDITYIN,
|
||||
name="Humidity In",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_LASTRAIN,
|
||||
name="Last Rain",
|
||||
icon="mdi:water",
|
||||
device_class=DEVICE_CLASS_TIMESTAMP,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_MAXDAILYGUST,
|
||||
name="Max Gust",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=SPEED_MILES_PER_HOUR,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_MONTHLYRAININ,
|
||||
name="Monthly Rain",
|
||||
icon="mdi:water",
|
||||
native_unit_of_measurement=PRECIPITATION_INCHES,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM25_24H,
|
||||
name="PM25 24h Avg",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=DEVICE_CLASS_PM25,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM25_IN,
|
||||
name="PM25 Indoor",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=DEVICE_CLASS_PM25,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM25_IN_24H,
|
||||
name="PM25 Indoor 24h Avg",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=DEVICE_CLASS_PM25,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM25,
|
||||
name="PM25",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=DEVICE_CLASS_PM25,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM10,
|
||||
name="Soil Humidity 10",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM1,
|
||||
name="Soil Humidity 1",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM2,
|
||||
name="Soil Humidity 2",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM3,
|
||||
name="Soil Humidity 3",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM4,
|
||||
name="Soil Humidity 4",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM5,
|
||||
name="Soil Humidity 5",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM6,
|
||||
name="Soil Humidity 6",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM7,
|
||||
name="Soil Humidity 7",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM8,
|
||||
name="Soil Humidity 8",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILHUM9,
|
||||
name="Soil Humidity 9",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP10F,
|
||||
name="Soil Temp 10",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP1F,
|
||||
name="Soil Temp 1",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP2F,
|
||||
name="Soil Temp 2",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP3F,
|
||||
name="Soil Temp 3",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP4F,
|
||||
name="Soil Temp 4",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP5F,
|
||||
name="Soil Temp 5",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP6F,
|
||||
name="Soil Temp 6",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP7F,
|
||||
name="Soil Temp 7",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP8F,
|
||||
name="Soil Temp 8",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOILTEMP9F,
|
||||
name="Soil Temp 9",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOLARRADIATION,
|
||||
name="Solar Rad",
|
||||
native_unit_of_measurement=IRRADIATION_WATTS_PER_SQUARE_METER,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_SOLARRADIATION_LX,
|
||||
name="Solar Rad",
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP10F,
|
||||
name="Temp 10",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP1F,
|
||||
name="Temp 1",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP2F,
|
||||
name="Temp 2",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP3F,
|
||||
name="Temp 3",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP4F,
|
||||
name="Temp 4",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP5F,
|
||||
name="Temp 5",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP6F,
|
||||
name="Temp 6",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP7F,
|
||||
name="Temp 7",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP8F,
|
||||
name="Temp 8",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMP9F,
|
||||
name="Temp 9",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMPF,
|
||||
name="Temp",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TEMPINF,
|
||||
name="Inside Temp",
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_TOTALRAININ,
|
||||
name="Lifetime Rain",
|
||||
icon="mdi:water",
|
||||
native_unit_of_measurement=PRECIPITATION_INCHES,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_UV,
|
||||
name="UV Index",
|
||||
native_unit_of_measurement="Index",
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WEEKLYRAININ,
|
||||
name="Weekly Rain",
|
||||
icon="mdi:water",
|
||||
native_unit_of_measurement=PRECIPITATION_INCHES,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDDIR,
|
||||
name="Wind Dir",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDDIR_AVG10M,
|
||||
name="Wind Dir Avg 10m",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDDIR_AVG2M,
|
||||
name="Wind Dir Avg 2m",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDGUSTDIR,
|
||||
name="Gust Dir",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDGUSTMPH,
|
||||
name="Wind Gust",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=SPEED_MILES_PER_HOUR,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDSPDMPH_AVG10M,
|
||||
name="Wind Avg 10m",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=SPEED_MILES_PER_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDSPDMPH_AVG2M,
|
||||
name="Wind Avg 2m",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=SPEED_MILES_PER_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDSPEEDMPH,
|
||||
name="Wind Speed",
|
||||
icon="mdi:weather-windy",
|
||||
native_unit_of_measurement=SPEED_MILES_PER_HOUR,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_YEARLYRAININ,
|
||||
name="Yearly Rain",
|
||||
icon="mdi:water",
|
||||
native_unit_of_measurement=PRECIPITATION_INCHES,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -23,24 +611,14 @@ async def async_setup_entry(
|
||||
"""Set up Ambient PWS sensors based on a config entry."""
|
||||
ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
|
||||
|
||||
sensor_list = []
|
||||
for mac_address, station in ambient.stations.items():
|
||||
for condition in station[ATTR_MONITORED_CONDITIONS]:
|
||||
name, unit, kind, device_class = SENSOR_TYPES[condition]
|
||||
if kind == SENSOR:
|
||||
sensor_list.append(
|
||||
AmbientWeatherSensor(
|
||||
ambient,
|
||||
mac_address,
|
||||
station[ATTR_NAME],
|
||||
condition,
|
||||
name,
|
||||
device_class,
|
||||
unit,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(sensor_list)
|
||||
async_add_entities(
|
||||
[
|
||||
AmbientWeatherSensor(ambient, mac_address, station[ATTR_NAME], description)
|
||||
for mac_address, station in ambient.stations.items()
|
||||
for description in SENSOR_DESCRIPTIONS
|
||||
if description.key in station[ATTR_LAST_DATA]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity):
|
||||
@@ -51,34 +629,20 @@ class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity):
|
||||
ambient: AmbientStation,
|
||||
mac_address: str,
|
||||
station_name: str,
|
||||
sensor_type: str,
|
||||
sensor_name: str,
|
||||
device_class: str | None,
|
||||
unit: str | None,
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(
|
||||
ambient, mac_address, station_name, sensor_type, sensor_name, device_class
|
||||
)
|
||||
super().__init__(ambient, mac_address, station_name, description)
|
||||
|
||||
self._attr_native_unit_of_measurement = unit
|
||||
if description.key == TYPE_SOLARRADIATION_LX:
|
||||
# Since TYPE_SOLARRADIATION and TYPE_SOLARRADIATION_LX will have the same
|
||||
# name in the UI, we influence the entity ID of TYPE_SOLARRADIATION_LX here
|
||||
# to differentiate them:
|
||||
self.entity_id = f"sensor.{station_name}_solar_rad_lx"
|
||||
|
||||
@callback
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Fetch new state data for the sensor."""
|
||||
if self._sensor_type == TYPE_SOLARRADIATION_LX:
|
||||
# If the user requests the solarradiation_lx sensor, use the
|
||||
# value of the solarradiation sensor and apply a very accurate
|
||||
# approximation of converting sunlight W/m^2 to lx:
|
||||
w_m2_brightness_val = self._ambient.stations[self._mac_address][
|
||||
ATTR_LAST_DATA
|
||||
].get(TYPE_SOLARRADIATION)
|
||||
|
||||
if w_m2_brightness_val is None:
|
||||
self._attr_native_value = None
|
||||
else:
|
||||
self._attr_native_value = round(float(w_m2_brightness_val) / 0.0079)
|
||||
else:
|
||||
self._attr_native_value = self._ambient.stations[self._mac_address][
|
||||
ATTR_LAST_DATA
|
||||
].get(self._sensor_type)
|
||||
self._attr_native_value = self._ambient.stations[self._mac_address][
|
||||
ATTR_LAST_DATA
|
||||
][self.entity_description.key]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Cette cl\u00e9 d'application est d\u00e9j\u00e0 utilis\u00e9e."
|
||||
"already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9"
|
||||
},
|
||||
"error": {
|
||||
"invalid_key": "Cl\u00e9 d'API et / ou cl\u00e9 d'application non valide",
|
||||
"invalid_key": "Cl\u00e9 API invalide",
|
||||
"no_devices": "Aucun appareil trouv\u00e9 dans le compte"
|
||||
},
|
||||
"step": {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"""Support for Amcrest IP cameras."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import threading
|
||||
from typing import Any, Callable
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from amcrest import AmcrestError, ApiWrapper, LoginError
|
||||
@@ -379,3 +380,4 @@ class AmcrestDevice:
|
||||
stream_source: str
|
||||
resolution: int
|
||||
control_light: bool
|
||||
channel: int = 0
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"""Support for Amcrest IP camera binary sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from amcrest import AmcrestError
|
||||
import voluptuous as vol
|
||||
@@ -111,6 +112,7 @@ BINARY_SENSORS: tuple[AmcrestSensorEntityDescription, ...] = (
|
||||
key=_ONLINE_KEY,
|
||||
name="Online",
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
should_poll=True,
|
||||
),
|
||||
)
|
||||
BINARY_SENSOR_KEYS = [description.key for description in BINARY_SENSORS]
|
||||
@@ -168,6 +170,7 @@ class AmcrestBinarySensor(BinarySensorEntity):
|
||||
"""Initialize entity."""
|
||||
self._signal_name = name
|
||||
self._api = device.api
|
||||
self._channel = device.channel
|
||||
self.entity_description: AmcrestSensorEntityDescription = entity_description
|
||||
|
||||
self._attr_name = f"{name} {entity_description.name}"
|
||||
@@ -191,12 +194,14 @@ class AmcrestBinarySensor(BinarySensorEntity):
|
||||
if not (self._api.available or self.is_on):
|
||||
return
|
||||
_LOGGER.debug(_UPDATE_MSG, self.name)
|
||||
|
||||
if self._api.available:
|
||||
# Send a command to the camera to test if we can still communicate with it.
|
||||
# Override of Http.command() in __init__.py will set self._api.available
|
||||
# accordingly.
|
||||
with suppress(AmcrestError):
|
||||
self._api.current_time # pylint: disable=pointless-statement
|
||||
self._update_unique_id()
|
||||
self._attr_is_on = self._api.available
|
||||
|
||||
def _update_others(self) -> None:
|
||||
@@ -204,6 +209,12 @@ class AmcrestBinarySensor(BinarySensorEntity):
|
||||
return
|
||||
_LOGGER.debug(_UPDATE_MSG, self.name)
|
||||
|
||||
try:
|
||||
self._update_unique_id()
|
||||
except AmcrestError as error:
|
||||
log_update_error(_LOGGER, "update", self.name, "binary sensor", error)
|
||||
return
|
||||
|
||||
event_code = self.entity_description.event_code
|
||||
if event_code is None:
|
||||
_LOGGER.error("Binary sensor %s event code not set", self.name)
|
||||
@@ -213,6 +224,16 @@ class AmcrestBinarySensor(BinarySensorEntity):
|
||||
self._attr_is_on = len(self._api.event_channels_happened(event_code)) > 0
|
||||
except AmcrestError as error:
|
||||
log_update_error(_LOGGER, "update", self.name, "binary sensor", error)
|
||||
return
|
||||
|
||||
def _update_unique_id(self) -> None:
|
||||
"""Set the unique id."""
|
||||
if self._attr_unique_id is None:
|
||||
serial_number = self._api.serial_number
|
||||
if serial_number:
|
||||
self._attr_unique_id = (
|
||||
f"{serial_number}-{self.entity_description.key}-{self._channel}"
|
||||
)
|
||||
|
||||
async def async_on_demand_update(self) -> None:
|
||||
"""Update state."""
|
||||
@@ -232,8 +253,6 @@ class AmcrestBinarySensor(BinarySensorEntity):
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to signals."""
|
||||
assert self.hass is not None
|
||||
|
||||
self._unsub_dispatcher.append(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiohttp import web
|
||||
from amcrest import AmcrestError
|
||||
@@ -13,9 +14,11 @@ from haffmpeg.camera import CameraMjpeg
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import SUPPORT_ON_OFF, SUPPORT_STREAM, Camera
|
||||
from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG, FFmpegManager
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_aiohttp_proxy_stream,
|
||||
async_aiohttp_proxy_web,
|
||||
@@ -32,6 +35,7 @@ from .const import (
|
||||
COMM_TIMEOUT,
|
||||
DATA_AMCREST,
|
||||
DEVICES,
|
||||
DOMAIN,
|
||||
SERVICE_UPDATE,
|
||||
SNAPSHOT_TIMEOUT,
|
||||
)
|
||||
@@ -132,7 +136,21 @@ async def async_setup_platform(
|
||||
|
||||
name = discovery_info[CONF_NAME]
|
||||
device = hass.data[DATA_AMCREST][DEVICES][name]
|
||||
async_add_entities([AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True)
|
||||
entity = AmcrestCam(name, device, hass.data[DATA_FFMPEG])
|
||||
|
||||
# 2021.9.0 introduced unique id's for the camera entity, but these were not
|
||||
# unique for different resolution streams. If any cameras were configured
|
||||
# with this version, update the old entity with the new unique id.
|
||||
serial_number = await hass.async_add_executor_job(lambda: device.api.serial_number) # type: ignore[no-any-return]
|
||||
serial_number = serial_number.strip()
|
||||
registry = entity_registry.async_get(hass)
|
||||
entity_id = registry.async_get_entity_id(CAMERA_DOMAIN, DOMAIN, serial_number)
|
||||
if entity_id is not None:
|
||||
_LOGGER.debug("Updating unique id for camera %s", entity_id)
|
||||
new_unique_id = f"{serial_number}-{device.resolution}-{device.channel}"
|
||||
registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
||||
|
||||
async_add_entities([entity], True)
|
||||
|
||||
|
||||
class CannotSnapshot(Exception):
|
||||
@@ -155,6 +173,7 @@ class AmcrestCam(Camera):
|
||||
self._ffmpeg_arguments = device.ffmpeg_arguments
|
||||
self._stream_source = device.stream_source
|
||||
self._resolution = device.resolution
|
||||
self._channel = device.channel
|
||||
self._token = self._auth = device.authentication
|
||||
self._control_light = device.control_light
|
||||
self._is_recording: bool = False
|
||||
@@ -180,7 +199,6 @@ class AmcrestCam(Camera):
|
||||
raise CannotSnapshot
|
||||
|
||||
async def _async_get_image(self) -> None:
|
||||
assert self.hass is not None
|
||||
try:
|
||||
# Send the request to snap a picture and return raw jpg data
|
||||
# Snapshot command needs a much longer read timeout than other commands.
|
||||
@@ -201,7 +219,6 @@ class AmcrestCam(Camera):
|
||||
self, width: int | None = None, height: int | None = None
|
||||
) -> bytes | None:
|
||||
"""Return a still image response from the camera."""
|
||||
assert self.hass is not None
|
||||
_LOGGER.debug("Take snapshot from %s", self._name)
|
||||
try:
|
||||
# Amcrest cameras only support one snapshot command at a time.
|
||||
@@ -226,7 +243,6 @@ class AmcrestCam(Camera):
|
||||
self, request: web.Request
|
||||
) -> web.StreamResponse | None:
|
||||
"""Return an MJPEG stream."""
|
||||
assert self.hass is not None
|
||||
# The snapshot implementation is handled by the parent class
|
||||
if self._stream_source == "snapshot":
|
||||
return await super().handle_async_mjpeg_stream(request)
|
||||
@@ -344,7 +360,6 @@ class AmcrestCam(Camera):
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to signals and add camera to list."""
|
||||
assert self.hass is not None
|
||||
self._unsub_dispatcher.extend(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
@@ -364,7 +379,6 @@ class AmcrestCam(Camera):
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove camera from list and disconnect from signals."""
|
||||
assert self.hass is not None
|
||||
self.hass.data[DATA_AMCREST][CAMERAS].remove(self.entity_id)
|
||||
for unsub_dispatcher in self._unsub_dispatcher:
|
||||
unsub_dispatcher()
|
||||
@@ -379,20 +393,25 @@ class AmcrestCam(Camera):
|
||||
try:
|
||||
if self._brand is None:
|
||||
resp = self._api.vendor_information.strip()
|
||||
if resp.startswith("vendor="):
|
||||
self._brand = resp.split("=")[-1]
|
||||
_LOGGER.debug("Assigned brand=%s", resp)
|
||||
if resp:
|
||||
self._brand = resp
|
||||
else:
|
||||
self._brand = "unknown"
|
||||
if self._model is None:
|
||||
resp = self._api.device_type.strip()
|
||||
_LOGGER.debug("Device_type=%s", resp)
|
||||
if resp.startswith("type="):
|
||||
self._model = resp.split("=")[-1]
|
||||
_LOGGER.debug("Assigned model=%s", resp)
|
||||
if resp:
|
||||
self._model = resp
|
||||
else:
|
||||
self._model = "unknown"
|
||||
if self._attr_unique_id is None:
|
||||
self._attr_unique_id = self._api.serial_number.strip()
|
||||
_LOGGER.debug("Assigned unique_id=%s", self._attr_unique_id)
|
||||
serial_number = self._api.serial_number.strip()
|
||||
if serial_number:
|
||||
self._attr_unique_id = (
|
||||
f"{serial_number}-{self._resolution}-{self._channel}"
|
||||
)
|
||||
_LOGGER.debug("Assigned unique_id=%s", self._attr_unique_id)
|
||||
self.is_streaming = self._get_video()
|
||||
self._is_recording = self._get_recording()
|
||||
self._motion_detection_enabled = self._get_motion_detection()
|
||||
@@ -428,57 +447,46 @@ class AmcrestCam(Camera):
|
||||
|
||||
async def async_enable_recording(self) -> None:
|
||||
"""Call the job and enable recording."""
|
||||
assert self.hass is not None
|
||||
await self.hass.async_add_executor_job(self._enable_recording, True)
|
||||
|
||||
async def async_disable_recording(self) -> None:
|
||||
"""Call the job and disable recording."""
|
||||
assert self.hass is not None
|
||||
await self.hass.async_add_executor_job(self._enable_recording, False)
|
||||
|
||||
async def async_enable_audio(self) -> None:
|
||||
"""Call the job and enable audio."""
|
||||
assert self.hass is not None
|
||||
await self.hass.async_add_executor_job(self._enable_audio, True)
|
||||
|
||||
async def async_disable_audio(self) -> None:
|
||||
"""Call the job and disable audio."""
|
||||
assert self.hass is not None
|
||||
await self.hass.async_add_executor_job(self._enable_audio, False)
|
||||
|
||||
async def async_enable_motion_recording(self) -> None:
|
||||
"""Call the job and enable motion recording."""
|
||||
assert self.hass is not None
|
||||
await self.hass.async_add_executor_job(self._enable_motion_recording, True)
|
||||
|
||||
async def async_disable_motion_recording(self) -> None:
|
||||
"""Call the job and disable motion recording."""
|
||||
assert self.hass is not None
|
||||
await self.hass.async_add_executor_job(self._enable_motion_recording, False)
|
||||
|
||||
async def async_goto_preset(self, preset: int) -> None:
|
||||
"""Call the job and move camera to preset position."""
|
||||
assert self.hass is not None
|
||||
await self.hass.async_add_executor_job(self._goto_preset, preset)
|
||||
|
||||
async def async_set_color_bw(self, color_bw: str) -> None:
|
||||
"""Call the job and set camera color mode."""
|
||||
assert self.hass is not None
|
||||
await self.hass.async_add_executor_job(self._set_color_bw, color_bw)
|
||||
|
||||
async def async_start_tour(self) -> None:
|
||||
"""Call the job and start camera tour."""
|
||||
assert self.hass is not None
|
||||
await self.hass.async_add_executor_job(self._start_tour, True)
|
||||
|
||||
async def async_stop_tour(self) -> None:
|
||||
"""Call the job and stop camera tour."""
|
||||
assert self.hass is not None
|
||||
await self.hass.async_add_executor_job(self._start_tour, False)
|
||||
|
||||
async def async_ptz_control(self, movement: str, travel_time: float) -> None:
|
||||
"""Move or zoom camera in specified direction."""
|
||||
assert self.hass is not None
|
||||
code = _ACTION[_MOV.index(movement)]
|
||||
|
||||
kwargs = {"code": code, "arg1": 0, "arg2": 0, "arg3": 0}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "amcrest",
|
||||
"name": "Amcrest",
|
||||
"documentation": "https://www.home-assistant.io/integrations/amcrest",
|
||||
"requirements": ["amcrest==1.8.0"],
|
||||
"requirements": ["amcrest==1.9.3"],
|
||||
"dependencies": ["ffmpeg"],
|
||||
"codeowners": ["@flacjacket"],
|
||||
"iot_class": "local_polling"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Support for Amcrest IP camera sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from amcrest import AmcrestError
|
||||
|
||||
@@ -77,6 +78,7 @@ class AmcrestSensor(SensorEntity):
|
||||
self.entity_description = description
|
||||
self._signal_name = name
|
||||
self._api = device.api
|
||||
self._channel = device.channel
|
||||
self._unsub_dispatcher: Callable[[], None] | None = None
|
||||
|
||||
self._attr_name = f"{name} {description.name}"
|
||||
@@ -94,7 +96,19 @@ class AmcrestSensor(SensorEntity):
|
||||
_LOGGER.debug("Updating %s sensor", self.name)
|
||||
|
||||
sensor_type = self.entity_description.key
|
||||
if self._attr_unique_id is None:
|
||||
serial_number = self._api.serial_number
|
||||
if serial_number:
|
||||
self._attr_unique_id = f"{serial_number}-{sensor_type}-{self._channel}"
|
||||
|
||||
try:
|
||||
if self._attr_unique_id is None:
|
||||
serial_number = self._api.serial_number
|
||||
if serial_number:
|
||||
self._attr_unique_id = (
|
||||
f"{serial_number}-{sensor_type}-{self._channel}"
|
||||
)
|
||||
|
||||
if sensor_type == SENSOR_PTZ_PRESET:
|
||||
self._attr_native_value = self._api.ptz_presets_count
|
||||
|
||||
@@ -129,7 +143,6 @@ class AmcrestSensor(SensorEntity):
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to update signal."""
|
||||
assert self.hass is not None
|
||||
self._unsub_dispatcher = async_dispatcher_connect(
|
||||
self.hass,
|
||||
service_signal(SERVICE_UPDATE, self._signal_name),
|
||||
|
||||
@@ -358,6 +358,7 @@ def adb_decorator(override_available=False):
|
||||
@functools.wraps(func)
|
||||
async def _adb_exception_catcher(self, *args, **kwargs):
|
||||
"""Call an ADB-related method and catch exceptions."""
|
||||
# pylint: disable=protected-access
|
||||
if not self.available and not override_available:
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
"""Support for APCUPSd sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from apcaccess.status import ALL_UNITS
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_RESOURCES,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -25,74 +31,360 @@ from . import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSOR_PREFIX = "UPS "
|
||||
SENSOR_TYPES = {
|
||||
"alarmdel": ["Alarm Delay", None, "mdi:alarm", None],
|
||||
"ambtemp": ["Ambient Temperature", None, "mdi:thermometer", None],
|
||||
"apc": ["Status Data", None, "mdi:information-outline", None],
|
||||
"apcmodel": ["Model", None, "mdi:information-outline", None],
|
||||
"badbatts": ["Bad Batteries", None, "mdi:information-outline", None],
|
||||
"battdate": ["Battery Replaced", None, "mdi:calendar-clock", None],
|
||||
"battstat": ["Battery Status", None, "mdi:information-outline", None],
|
||||
"battv": ["Battery Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"bcharge": ["Battery", PERCENTAGE, "mdi:battery", None],
|
||||
"cable": ["Cable Type", None, "mdi:ethernet-cable", None],
|
||||
"cumonbatt": ["Total Time on Battery", None, "mdi:timer-outline", None],
|
||||
"date": ["Status Date", None, "mdi:calendar-clock", None],
|
||||
"dipsw": ["Dip Switch Settings", None, "mdi:information-outline", None],
|
||||
"dlowbatt": ["Low Battery Signal", None, "mdi:clock-alert", None],
|
||||
"driver": ["Driver", None, "mdi:information-outline", None],
|
||||
"dshutd": ["Shutdown Delay", None, "mdi:timer-outline", None],
|
||||
"dwake": ["Wake Delay", None, "mdi:timer-outline", None],
|
||||
"endapc": ["Date and Time", None, "mdi:calendar-clock", None],
|
||||
"extbatts": ["External Batteries", None, "mdi:information-outline", None],
|
||||
"firmware": ["Firmware Version", None, "mdi:information-outline", None],
|
||||
"hitrans": ["Transfer High", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"hostname": ["Hostname", None, "mdi:information-outline", None],
|
||||
"humidity": ["Ambient Humidity", PERCENTAGE, "mdi:water-percent", None],
|
||||
"itemp": ["Internal Temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE],
|
||||
"lastxfer": ["Last Transfer", None, "mdi:transfer", None],
|
||||
"linefail": ["Input Voltage Status", None, "mdi:information-outline", None],
|
||||
"linefreq": ["Line Frequency", FREQUENCY_HERTZ, "mdi:information-outline", None],
|
||||
"linev": ["Input Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"loadpct": ["Load", PERCENTAGE, "mdi:gauge", None],
|
||||
"loadapnt": ["Load Apparent Power", PERCENTAGE, "mdi:gauge", None],
|
||||
"lotrans": ["Transfer Low", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"mandate": ["Manufacture Date", None, "mdi:calendar", None],
|
||||
"masterupd": ["Master Update", None, "mdi:information-outline", None],
|
||||
"maxlinev": ["Input Voltage High", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"maxtime": ["Battery Timeout", None, "mdi:timer-off-outline", None],
|
||||
"mbattchg": ["Battery Shutdown", PERCENTAGE, "mdi:battery-alert", None],
|
||||
"minlinev": ["Input Voltage Low", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"mintimel": ["Shutdown Time", None, "mdi:timer-outline", None],
|
||||
"model": ["Model", None, "mdi:information-outline", None],
|
||||
"nombattv": ["Battery Nominal Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"nominv": ["Nominal Input Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"nomoutv": ["Nominal Output Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"nompower": ["Nominal Output Power", POWER_WATT, "mdi:flash", None],
|
||||
"nomapnt": ["Nominal Apparent Power", POWER_VOLT_AMPERE, "mdi:flash", None],
|
||||
"numxfers": ["Transfer Count", None, "mdi:counter", None],
|
||||
"outcurnt": ["Output Current", ELECTRIC_CURRENT_AMPERE, "mdi:flash", None],
|
||||
"outputv": ["Output Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"reg1": ["Register 1 Fault", None, "mdi:information-outline", None],
|
||||
"reg2": ["Register 2 Fault", None, "mdi:information-outline", None],
|
||||
"reg3": ["Register 3 Fault", None, "mdi:information-outline", None],
|
||||
"retpct": ["Restore Requirement", PERCENTAGE, "mdi:battery-alert", None],
|
||||
"selftest": ["Last Self Test", None, "mdi:calendar-clock", None],
|
||||
"sense": ["Sensitivity", None, "mdi:information-outline", None],
|
||||
"serialno": ["Serial Number", None, "mdi:information-outline", None],
|
||||
"starttime": ["Startup Time", None, "mdi:calendar-clock", None],
|
||||
"statflag": ["Status Flag", None, "mdi:information-outline", None],
|
||||
"status": ["Status", None, "mdi:information-outline", None],
|
||||
"stesti": ["Self Test Interval", None, "mdi:information-outline", None],
|
||||
"timeleft": ["Time Left", None, "mdi:clock-alert", None],
|
||||
"tonbatt": ["Time on Battery", None, "mdi:timer-outline", None],
|
||||
"upsmode": ["Mode", None, "mdi:information-outline", None],
|
||||
"upsname": ["Name", None, "mdi:information-outline", None],
|
||||
"version": ["Daemon Info", None, "mdi:information-outline", None],
|
||||
"xoffbat": ["Transfer from Battery", None, "mdi:transfer", None],
|
||||
"xoffbatt": ["Transfer from Battery", None, "mdi:transfer", None],
|
||||
"xonbatt": ["Transfer to Battery", None, "mdi:transfer", None],
|
||||
}
|
||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key="alarmdel",
|
||||
name="Alarm Delay",
|
||||
icon="mdi:alarm",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="ambtemp",
|
||||
name="Ambient Temperature",
|
||||
icon="mdi:thermometer",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="apc",
|
||||
name="Status Data",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="apcmodel",
|
||||
name="Model",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="badbatts",
|
||||
name="Bad Batteries",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="battdate",
|
||||
name="Battery Replaced",
|
||||
icon="mdi:calendar-clock",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="battstat",
|
||||
name="Battery Status",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="battv",
|
||||
name="Battery Voltage",
|
||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="bcharge",
|
||||
name="Battery",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:battery",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="cable",
|
||||
name="Cable Type",
|
||||
icon="mdi:ethernet-cable",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="cumonbatt",
|
||||
name="Total Time on Battery",
|
||||
icon="mdi:timer-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="date",
|
||||
name="Status Date",
|
||||
icon="mdi:calendar-clock",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="dipsw",
|
||||
name="Dip Switch Settings",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="dlowbatt",
|
||||
name="Low Battery Signal",
|
||||
icon="mdi:clock-alert",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="driver",
|
||||
name="Driver",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="dshutd",
|
||||
name="Shutdown Delay",
|
||||
icon="mdi:timer-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="dwake",
|
||||
name="Wake Delay",
|
||||
icon="mdi:timer-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="endapc",
|
||||
name="Date and Time",
|
||||
icon="mdi:calendar-clock",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="extbatts",
|
||||
name="External Batteries",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="firmware",
|
||||
name="Firmware Version",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="hitrans",
|
||||
name="Transfer High",
|
||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="hostname",
|
||||
name="Hostname",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="humidity",
|
||||
name="Ambient Humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:water-percent",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="itemp",
|
||||
name="Internal Temperature",
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="lastxfer",
|
||||
name="Last Transfer",
|
||||
icon="mdi:transfer",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="linefail",
|
||||
name="Input Voltage Status",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="linefreq",
|
||||
name="Line Frequency",
|
||||
native_unit_of_measurement=FREQUENCY_HERTZ,
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="linev",
|
||||
name="Input Voltage",
|
||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="loadpct",
|
||||
name="Load",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:gauge",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="loadapnt",
|
||||
name="Load Apparent Power",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:gauge",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="lotrans",
|
||||
name="Transfer Low",
|
||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="mandate",
|
||||
name="Manufacture Date",
|
||||
icon="mdi:calendar",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="masterupd",
|
||||
name="Master Update",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="maxlinev",
|
||||
name="Input Voltage High",
|
||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="maxtime",
|
||||
name="Battery Timeout",
|
||||
icon="mdi:timer-off-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="mbattchg",
|
||||
name="Battery Shutdown",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:battery-alert",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="minlinev",
|
||||
name="Input Voltage Low",
|
||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="mintimel",
|
||||
name="Shutdown Time",
|
||||
icon="mdi:timer-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="model",
|
||||
name="Model",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="nombattv",
|
||||
name="Battery Nominal Voltage",
|
||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="nominv",
|
||||
name="Nominal Input Voltage",
|
||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="nomoutv",
|
||||
name="Nominal Output Voltage",
|
||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="nompower",
|
||||
name="Nominal Output Power",
|
||||
native_unit_of_measurement=POWER_WATT,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="nomapnt",
|
||||
name="Nominal Apparent Power",
|
||||
native_unit_of_measurement=POWER_VOLT_AMPERE,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="numxfers",
|
||||
name="Transfer Count",
|
||||
icon="mdi:counter",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="outcurnt",
|
||||
name="Output Current",
|
||||
native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="outputv",
|
||||
name="Output Voltage",
|
||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||
icon="mdi:flash",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="reg1",
|
||||
name="Register 1 Fault",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="reg2",
|
||||
name="Register 2 Fault",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="reg3",
|
||||
name="Register 3 Fault",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="retpct",
|
||||
name="Restore Requirement",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:battery-alert",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="selftest",
|
||||
name="Last Self Test",
|
||||
icon="mdi:calendar-clock",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="sense",
|
||||
name="Sensitivity",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="serialno",
|
||||
name="Serial Number",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="starttime",
|
||||
name="Startup Time",
|
||||
icon="mdi:calendar-clock",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="statflag",
|
||||
name="Status Flag",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="status",
|
||||
name="Status",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="stesti",
|
||||
name="Self Test Interval",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="timeleft",
|
||||
name="Time Left",
|
||||
icon="mdi:clock-alert",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="tonbatt",
|
||||
name="Time on Battery",
|
||||
icon="mdi:timer-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="upsmode",
|
||||
name="Mode",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="upsname",
|
||||
name="Name",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="version",
|
||||
name="Daemon Info",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="xoffbat",
|
||||
name="Transfer from Battery",
|
||||
icon="mdi:transfer",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="xoffbatt",
|
||||
name="Transfer from Battery",
|
||||
icon="mdi:transfer",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="xonbatt",
|
||||
name="Transfer to Battery",
|
||||
icon="mdi:transfer",
|
||||
),
|
||||
)
|
||||
SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
|
||||
|
||||
SPECIFIC_UNITS = {"ITEMP": TEMP_CELSIUS}
|
||||
INFERRED_UNITS = {
|
||||
@@ -111,7 +403,7 @@ INFERRED_UNITS = {
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_RESOURCES, default=[]): vol.All(
|
||||
cv.ensure_list, [vol.In(SENSOR_TYPES)]
|
||||
cv.ensure_list, [vol.In(SENSOR_KEYS)]
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -120,25 +412,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the APCUPSd sensors."""
|
||||
apcups_data = hass.data[DOMAIN]
|
||||
entities = []
|
||||
resources = config[CONF_RESOURCES]
|
||||
|
||||
for resource in config[CONF_RESOURCES]:
|
||||
sensor_type = resource.lower()
|
||||
|
||||
if sensor_type not in SENSOR_TYPES:
|
||||
SENSOR_TYPES[sensor_type] = [
|
||||
sensor_type.title(),
|
||||
"",
|
||||
"mdi:information-outline",
|
||||
]
|
||||
|
||||
if sensor_type.upper() not in apcups_data.status:
|
||||
for resource in resources:
|
||||
if resource.upper() not in apcups_data.status:
|
||||
_LOGGER.warning(
|
||||
"Sensor type: %s does not appear in the APCUPSd status output",
|
||||
sensor_type,
|
||||
resource,
|
||||
)
|
||||
|
||||
entities.append(APCUPSdSensor(apcups_data, sensor_type))
|
||||
entities = [
|
||||
APCUPSdSensor(apcups_data, description)
|
||||
for description in SENSOR_TYPES
|
||||
if description.key in resources
|
||||
]
|
||||
|
||||
add_entities(entities, True)
|
||||
|
||||
@@ -159,22 +446,18 @@ def infer_unit(value):
|
||||
class APCUPSdSensor(SensorEntity):
|
||||
"""Representation of a sensor entity for APCUPSd status values."""
|
||||
|
||||
def __init__(self, data, sensor_type):
|
||||
def __init__(self, data, description: SensorEntityDescription):
|
||||
"""Initialize the sensor."""
|
||||
self.entity_description = description
|
||||
self._data = data
|
||||
self.type = sensor_type
|
||||
self._attr_name = SENSOR_PREFIX + SENSOR_TYPES[sensor_type][0]
|
||||
self._attr_icon = SENSOR_TYPES[self.type][2]
|
||||
self._attr_native_unit_of_measurement = SENSOR_TYPES[sensor_type][1]
|
||||
self._attr_device_class = SENSOR_TYPES[sensor_type][3]
|
||||
self._attr_name = f"{SENSOR_PREFIX}{description.name}"
|
||||
|
||||
def update(self):
|
||||
"""Get the latest status and use it to update our sensor state."""
|
||||
if self.type.upper() not in self._data.status:
|
||||
key = self.entity_description.key.upper()
|
||||
if key not in self._data.status:
|
||||
self._attr_native_value = None
|
||||
else:
|
||||
self._attr_native_value, inferred_unit = infer_unit(
|
||||
self._data.status[self.type.upper()]
|
||||
)
|
||||
if not self._attr_native_unit_of_measurement:
|
||||
self._attr_native_value, inferred_unit = infer_unit(self._data.status[key])
|
||||
if not self.native_unit_of_measurement:
|
||||
self._attr_native_unit_of_measurement = inferred_unit
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Rest API for Home Assistant."""
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -15,10 +15,6 @@ from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_TIME_CHANGED,
|
||||
HTTP_BAD_REQUEST,
|
||||
HTTP_CREATED,
|
||||
HTTP_NOT_FOUND,
|
||||
HTTP_OK,
|
||||
MATCH_ALL,
|
||||
URL_API,
|
||||
URL_API_COMPONENTS,
|
||||
@@ -30,15 +26,12 @@ from homeassistant.const import (
|
||||
URL_API_STATES,
|
||||
URL_API_STREAM,
|
||||
URL_API_TEMPLATE,
|
||||
__version__,
|
||||
)
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.exceptions import ServiceNotFound, TemplateError, Unauthorized
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.json import JSONEncoder
|
||||
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
||||
from homeassistant.helpers.service import async_get_all_descriptions
|
||||
from homeassistant.helpers.system_info import async_get_system_info
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -97,6 +90,7 @@ class APIEventStream(HomeAssistantView):
|
||||
|
||||
async def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
# pylint: disable=no-self-use
|
||||
if not request["hass_user"].is_admin:
|
||||
raise Unauthorized()
|
||||
hass = request.app["hass"]
|
||||
@@ -173,7 +167,11 @@ class APIConfigView(HomeAssistantView):
|
||||
|
||||
|
||||
class APIDiscoveryView(HomeAssistantView):
|
||||
"""View to provide Discovery information."""
|
||||
"""
|
||||
View to provide Discovery information.
|
||||
|
||||
DEPRECATED: To be removed in 2022.1
|
||||
"""
|
||||
|
||||
requires_auth = False
|
||||
url = URL_API_DISCOVERY_INFO
|
||||
@@ -181,32 +179,18 @@ class APIDiscoveryView(HomeAssistantView):
|
||||
|
||||
async def get(self, request):
|
||||
"""Get discovery information."""
|
||||
hass = request.app["hass"]
|
||||
uuid = await hass.helpers.instance_id.async_get()
|
||||
system_info = await async_get_system_info(hass)
|
||||
|
||||
data = {
|
||||
ATTR_UUID: uuid,
|
||||
ATTR_BASE_URL: None,
|
||||
ATTR_EXTERNAL_URL: None,
|
||||
ATTR_INTERNAL_URL: None,
|
||||
ATTR_LOCATION_NAME: hass.config.location_name,
|
||||
ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE],
|
||||
# always needs authentication
|
||||
ATTR_REQUIRES_API_PASSWORD: True,
|
||||
ATTR_VERSION: __version__,
|
||||
}
|
||||
|
||||
with suppress(NoURLAvailableError):
|
||||
data["external_url"] = get_url(hass, allow_internal=False)
|
||||
|
||||
with suppress(NoURLAvailableError):
|
||||
data["internal_url"] = get_url(hass, allow_external=False)
|
||||
|
||||
# Set old base URL based on external or internal
|
||||
data["base_url"] = data["external_url"] or data["internal_url"]
|
||||
|
||||
return self.json(data)
|
||||
return self.json(
|
||||
{
|
||||
ATTR_UUID: "",
|
||||
ATTR_BASE_URL: "",
|
||||
ATTR_EXTERNAL_URL: "",
|
||||
ATTR_INTERNAL_URL: "",
|
||||
ATTR_LOCATION_NAME: "",
|
||||
ATTR_INSTALLATION_TYPE: "",
|
||||
ATTR_REQUIRES_API_PASSWORD: True,
|
||||
ATTR_VERSION: "",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class APIStatesView(HomeAssistantView):
|
||||
@@ -244,7 +228,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
state = request.app["hass"].states.get(entity_id)
|
||||
if state:
|
||||
return self.json(state)
|
||||
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
|
||||
return self.json_message("Entity not found.", HTTPStatus.NOT_FOUND)
|
||||
|
||||
async def post(self, request, entity_id):
|
||||
"""Update state of entity."""
|
||||
@@ -254,12 +238,12 @@ class APIEntityStateView(HomeAssistantView):
|
||||
try:
|
||||
data = await request.json()
|
||||
except ValueError:
|
||||
return self.json_message("Invalid JSON specified.", HTTP_BAD_REQUEST)
|
||||
return self.json_message("Invalid JSON specified.", HTTPStatus.BAD_REQUEST)
|
||||
|
||||
new_state = data.get("state")
|
||||
|
||||
if new_state is None:
|
||||
return self.json_message("No state specified.", HTTP_BAD_REQUEST)
|
||||
return self.json_message("No state specified.", HTTPStatus.BAD_REQUEST)
|
||||
|
||||
attributes = data.get("attributes")
|
||||
force_update = data.get("force_update", False)
|
||||
@@ -272,7 +256,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
)
|
||||
|
||||
# Read the state back for our response
|
||||
status_code = HTTP_CREATED if is_new_state else HTTP_OK
|
||||
status_code = HTTPStatus.CREATED if is_new_state else HTTPStatus.OK
|
||||
resp = self.json(hass.states.get(entity_id), status_code)
|
||||
|
||||
resp.headers.add("Location", f"/api/states/{entity_id}")
|
||||
@@ -286,7 +270,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
raise Unauthorized(entity_id=entity_id)
|
||||
if request.app["hass"].states.async_remove(entity_id):
|
||||
return self.json_message("Entity removed.")
|
||||
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
|
||||
return self.json_message("Entity not found.", HTTPStatus.NOT_FOUND)
|
||||
|
||||
|
||||
class APIEventListenersView(HomeAssistantView):
|
||||
@@ -316,12 +300,12 @@ class APIEventView(HomeAssistantView):
|
||||
event_data = json.loads(body) if body else None
|
||||
except ValueError:
|
||||
return self.json_message(
|
||||
"Event data should be valid JSON.", HTTP_BAD_REQUEST
|
||||
"Event data should be valid JSON.", HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
if event_data is not None and not isinstance(event_data, dict):
|
||||
return self.json_message(
|
||||
"Event data should be a JSON object", HTTP_BAD_REQUEST
|
||||
"Event data should be a JSON object", HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
# Special case handling for event STATE_CHANGED
|
||||
@@ -368,7 +352,9 @@ class APIDomainServicesView(HomeAssistantView):
|
||||
try:
|
||||
data = json.loads(body) if body else None
|
||||
except ValueError:
|
||||
return self.json_message("Data should be valid JSON.", HTTP_BAD_REQUEST)
|
||||
return self.json_message(
|
||||
"Data should be valid JSON.", HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
context = self.context(request)
|
||||
|
||||
@@ -416,7 +402,7 @@ class APITemplateView(HomeAssistantView):
|
||||
return tpl.async_render(variables=data.get("variables"), parse_result=False)
|
||||
except (ValueError, TemplateError) as ex:
|
||||
return self.json_message(
|
||||
f"Error rendering template: {ex}", HTTP_BAD_REQUEST
|
||||
f"Error rendering template: {ex}", HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
@@ -428,6 +414,7 @@ class APIErrorLog(HomeAssistantView):
|
||||
|
||||
async def get(self, request):
|
||||
"""Retrieve API error log."""
|
||||
# pylint: disable=no-self-use
|
||||
if not request["hass_user"].is_admin:
|
||||
raise Unauthorized()
|
||||
return web.FileResponse(request.app["hass"].data[DATA_LOGGING])
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
||||
"requirements": ["pyatv==0.8.2"],
|
||||
"zeroconf": ["_mediaremotetv._tcp.local.", "_touch-able._tcp.local."],
|
||||
"after_dependencies": ["discovery"],
|
||||
"codeowners": ["@postlund"],
|
||||
"iot_class": "local_push"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured_device": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9",
|
||||
"already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
|
||||
"already_in_progress": "La configuration est d\u00e9j\u00e0 en cours",
|
||||
"backoff": "L'appareil n'accepte pas les demandes d'appariement pour le moment (vous avez peut-\u00eatre saisi un code PIN non valide trop de fois), r\u00e9essayez plus tard.",
|
||||
"device_did_not_pair": "Aucune tentative pour terminer l'appairage n'a \u00e9t\u00e9 effectu\u00e9e \u00e0 partir de l'appareil.",
|
||||
@@ -11,10 +11,10 @@
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
|
||||
"invalid_auth": "Autentification invalide",
|
||||
"no_devices_found": "Aucun appareil d\u00e9tect\u00e9 sur le r\u00e9seau",
|
||||
"invalid_auth": "Authentification invalide",
|
||||
"no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau",
|
||||
"no_usable_service": "Un dispositif a \u00e9t\u00e9 trouv\u00e9, mais aucun moyen d\u2019\u00e9tablir un lien avec lui. Si vous continuez \u00e0 voir ce message, essayez de sp\u00e9cifier son adresse IP ou de red\u00e9marrer votre Apple TV.",
|
||||
"unknown": "Erreur innatendue"
|
||||
"unknown": "Erreur inattendue"
|
||||
},
|
||||
"flow_title": "Apple TV: {name}",
|
||||
"step": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "apprise",
|
||||
"name": "Apprise",
|
||||
"documentation": "https://www.home-assistant.io/integrations/apprise",
|
||||
"requirements": ["apprise==0.9.4"],
|
||||
"requirements": ["apprise==0.9.5.1"],
|
||||
"codeowners": ["@caronc"],
|
||||
"iot_class": "cloud_push"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
"""Support for AquaLogic sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_MONITORED_CONDITIONS,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -16,40 +23,88 @@ import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import DOMAIN, UPDATE_TOPIC
|
||||
|
||||
TEMP_UNITS = [TEMP_CELSIUS, TEMP_FAHRENHEIT]
|
||||
PERCENT_UNITS = [PERCENTAGE, PERCENTAGE]
|
||||
SALT_UNITS = ["g/L", "PPM"]
|
||||
WATT_UNITS = [POWER_WATT, POWER_WATT]
|
||||
NO_UNITS = [None, None]
|
||||
|
||||
# sensor_type [ description, unit, icon, device_class ]
|
||||
# sensor_type corresponds to property names in aqualogic.core.AquaLogic
|
||||
SENSOR_TYPES = {
|
||||
"air_temp": ["Air Temperature", TEMP_UNITS, None, DEVICE_CLASS_TEMPERATURE],
|
||||
"pool_temp": [
|
||||
"Pool Temperature",
|
||||
TEMP_UNITS,
|
||||
"mdi:oil-temperature",
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
],
|
||||
"spa_temp": [
|
||||
"Spa Temperature",
|
||||
TEMP_UNITS,
|
||||
"mdi:oil-temperature",
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
],
|
||||
"pool_chlorinator": ["Pool Chlorinator", PERCENT_UNITS, "mdi:gauge", None],
|
||||
"spa_chlorinator": ["Spa Chlorinator", PERCENT_UNITS, "mdi:gauge", None],
|
||||
"salt_level": ["Salt Level", SALT_UNITS, "mdi:gauge", None],
|
||||
"pump_speed": ["Pump Speed", PERCENT_UNITS, "mdi:speedometer", None],
|
||||
"pump_power": ["Pump Power", WATT_UNITS, "mdi:gauge", None],
|
||||
"status": ["Status", NO_UNITS, "mdi:alert", None],
|
||||
}
|
||||
@dataclass
|
||||
class AquaLogicSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes AquaLogic sensor entity."""
|
||||
|
||||
unit_metric: str | None = None
|
||||
unit_imperial: str | None = None
|
||||
|
||||
|
||||
# keys correspond to property names in aqualogic.core.AquaLogic
|
||||
SENSOR_TYPES: tuple[AquaLogicSensorEntityDescription, ...] = (
|
||||
AquaLogicSensorEntityDescription(
|
||||
key="air_temp",
|
||||
name="Air Temperature",
|
||||
unit_metric=TEMP_CELSIUS,
|
||||
unit_imperial=TEMP_FAHRENHEIT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
),
|
||||
AquaLogicSensorEntityDescription(
|
||||
key="pool_temp",
|
||||
name="Pool Temperature",
|
||||
unit_metric=TEMP_CELSIUS,
|
||||
unit_imperial=TEMP_FAHRENHEIT,
|
||||
icon="mdi:oil-temperature",
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
),
|
||||
AquaLogicSensorEntityDescription(
|
||||
key="spa_temp",
|
||||
name="Spa Temperature",
|
||||
unit_metric=TEMP_CELSIUS,
|
||||
unit_imperial=TEMP_FAHRENHEIT,
|
||||
icon="mdi:oil-temperature",
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
),
|
||||
AquaLogicSensorEntityDescription(
|
||||
key="pool_chlorinator",
|
||||
name="Pool Chlorinator",
|
||||
unit_metric=PERCENTAGE,
|
||||
unit_imperial=PERCENTAGE,
|
||||
icon="mdi:gauge",
|
||||
),
|
||||
AquaLogicSensorEntityDescription(
|
||||
key="spa_chlorinator",
|
||||
name="Spa Chlorinator",
|
||||
unit_metric=PERCENTAGE,
|
||||
unit_imperial=PERCENTAGE,
|
||||
icon="mdi:gauge",
|
||||
),
|
||||
AquaLogicSensorEntityDescription(
|
||||
key="salt_level",
|
||||
name="Salt Level",
|
||||
unit_metric="g/L",
|
||||
unit_imperial="PPM",
|
||||
icon="mdi:gauge",
|
||||
),
|
||||
AquaLogicSensorEntityDescription(
|
||||
key="pump_speed",
|
||||
name="Pump Speed",
|
||||
unit_metric=PERCENTAGE,
|
||||
unit_imperial=PERCENTAGE,
|
||||
icon="mdi:speedometer",
|
||||
),
|
||||
AquaLogicSensorEntityDescription(
|
||||
key="pump_power",
|
||||
name="Pump Power",
|
||||
unit_metric=POWER_WATT,
|
||||
unit_imperial=POWER_WATT,
|
||||
icon="mdi:gauge",
|
||||
),
|
||||
AquaLogicSensorEntityDescription(
|
||||
key="status",
|
||||
name="Status",
|
||||
icon="mdi:alert",
|
||||
),
|
||||
)
|
||||
|
||||
SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All(
|
||||
cv.ensure_list, [vol.In(SENSOR_TYPES)]
|
||||
vol.Required(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All(
|
||||
cv.ensure_list, [vol.In(SENSOR_KEYS)]
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -57,26 +112,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the sensor platform."""
|
||||
sensors = []
|
||||
|
||||
processor = hass.data[DOMAIN]
|
||||
for sensor_type in config[CONF_MONITORED_CONDITIONS]:
|
||||
sensors.append(AquaLogicSensor(processor, sensor_type))
|
||||
monitored_conditions = config[CONF_MONITORED_CONDITIONS]
|
||||
|
||||
async_add_entities(sensors)
|
||||
entities = [
|
||||
AquaLogicSensor(processor, description)
|
||||
for description in SENSOR_TYPES
|
||||
if description.key in monitored_conditions
|
||||
]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AquaLogicSensor(SensorEntity):
|
||||
"""Sensor implementation for the AquaLogic component."""
|
||||
|
||||
entity_description: AquaLogicSensorEntityDescription
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, processor, sensor_type):
|
||||
def __init__(self, processor, description: AquaLogicSensorEntityDescription):
|
||||
"""Initialize sensor."""
|
||||
self.entity_description = description
|
||||
self._processor = processor
|
||||
self._type = sensor_type
|
||||
self._attr_name = f"AquaLogic {SENSOR_TYPES[sensor_type][0]}"
|
||||
self._attr_icon = SENSOR_TYPES[sensor_type][2]
|
||||
self._attr_name = f"AquaLogic {description.name}"
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
@@ -92,11 +150,15 @@ class AquaLogicSensor(SensorEntity):
|
||||
panel = self._processor.panel
|
||||
if panel is not None:
|
||||
if panel.is_metric:
|
||||
self._attr_native_unit_of_measurement = SENSOR_TYPES[self._type][1][0]
|
||||
self._attr_native_unit_of_measurement = (
|
||||
self.entity_description.unit_metric
|
||||
)
|
||||
else:
|
||||
self._attr_native_unit_of_measurement = SENSOR_TYPES[self._type][1][1]
|
||||
self._attr_native_unit_of_measurement = (
|
||||
self.entity_description.unit_imperial
|
||||
)
|
||||
|
||||
self._attr_native_value = getattr(panel, self._type)
|
||||
self._attr_native_value = getattr(panel, self.entity_description.key)
|
||||
self.async_write_ha_state()
|
||||
else:
|
||||
self._attr_native_unit_of_measurement = None
|
||||
|
||||
@@ -5,7 +5,10 @@ from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.automation import AutomationActionType
|
||||
from homeassistant.components.automation import (
|
||||
AutomationActionType,
|
||||
AutomationTriggerInfo,
|
||||
)
|
||||
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
@@ -57,10 +60,10 @@ async def async_attach_trigger(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
action: AutomationActionType,
|
||||
automation_info: dict,
|
||||
automation_info: AutomationTriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
|
||||
trigger_data = automation_info["trigger_data"]
|
||||
job = HassJob(action)
|
||||
|
||||
if config[CONF_TYPE] == "turn_on":
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "L'appareil \u00e9tait d\u00e9j\u00e0 configur\u00e9.",
|
||||
"already_in_progress": "Le flux de configuration de l'appareil est d\u00e9j\u00e0 en cours.",
|
||||
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9",
|
||||
"already_in_progress": "La configuration est d\u00e9j\u00e0 en cours",
|
||||
"cannot_connect": "\u00c9chec de connexion"
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -88,6 +88,7 @@ class ArestSwitchBase(SwitchEntity):
|
||||
self._resource = resource
|
||||
self._attr_name = f"{location.title()} {name.title()}"
|
||||
self._attr_available = True
|
||||
self._attr_is_on = False
|
||||
|
||||
|
||||
class ArestSwitchFunction(ArestSwitchBase):
|
||||
|
||||
@@ -81,7 +81,7 @@ async def async_setup(hass, config):
|
||||
options = {}
|
||||
mode = conf.get(CONF_MODE, MODE_ROUTER)
|
||||
for name, value in conf.items():
|
||||
if name in ([CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]):
|
||||
if name in [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]:
|
||||
if name == CONF_REQUIRE_IP and mode != MODE_AP:
|
||||
continue
|
||||
options[name] = value
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Represent the AsusWrt router."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any, Callable
|
||||
from typing import Any
|
||||
|
||||
from aioasuswrt.asuswrt import AsusWrt
|
||||
|
||||
@@ -373,7 +374,7 @@ class AsusWrtRouter:
|
||||
"""Update router options."""
|
||||
req_reload = False
|
||||
for name, new_opt in new_options.items():
|
||||
if name in (CONF_REQ_RELOAD):
|
||||
if name in CONF_REQ_RELOAD:
|
||||
old_opt = self._options.get(name)
|
||||
if not old_opt or old_opt != new_opt:
|
||||
req_reload = True
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd",
|
||||
"pwd_and_ssh": "\u05e1\u05e4\u05e7 \u05e8\u05e7 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d0\u05d5 \u05e7\u05d5\u05d1\u05e5 \u05de\u05e4\u05ea\u05d7 SSH",
|
||||
"pwd_or_ssh": "\u05d0\u05e0\u05d0 \u05e1\u05e4\u05e7 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d0\u05d5 \u05e7\u05d5\u05d1\u05e5 \u05de\u05e4\u05ea\u05d7 SSH",
|
||||
"ssh_not_file": "\u05e7\u05d5\u05d1\u05e5 \u05de\u05e4\u05ea\u05d7 SSH \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0",
|
||||
"unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4"
|
||||
},
|
||||
"step": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Un seul appareil Atag peut \u00eatre ajout\u00e9 \u00e0 Home Assistant"
|
||||
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u00c9chec de connexion",
|
||||
@@ -10,7 +10,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Nom d'h\u00f4te ou adresse IP",
|
||||
"host": "H\u00f4te",
|
||||
"port": "Port"
|
||||
},
|
||||
"title": "Se connecter \u00e0 l'appareil"
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
"""Support for August binary sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, SOURCE_PUBNUB, ActivityType
|
||||
from yalexs.activity import (
|
||||
ACTION_DOORBELL_CALL_MISSED,
|
||||
SOURCE_PUBNUB,
|
||||
Activity,
|
||||
ActivityType,
|
||||
)
|
||||
from yalexs.doorbell import DoorbellDetail
|
||||
from yalexs.lock import LockDoorStatus
|
||||
from yalexs.util import update_lock_detail_from_activity
|
||||
|
||||
from homeassistant.components.august import AugustData
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASS_CONNECTIVITY,
|
||||
DEVICE_CLASS_DOOR,
|
||||
DEVICE_CLASS_MOTION,
|
||||
DEVICE_CLASS_OCCUPANCY,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
@@ -27,7 +40,7 @@ TIME_TO_RECHECK_DETECTION = timedelta(
|
||||
)
|
||||
|
||||
|
||||
def _retrieve_online_state(data, detail):
|
||||
def _retrieve_online_state(data: AugustData, detail: DoorbellDetail) -> bool:
|
||||
"""Get the latest state of the sensor."""
|
||||
# The doorbell will go into standby mode when there is no motion
|
||||
# for a short while. It will wake by itself when needed so we need
|
||||
@@ -36,7 +49,7 @@ def _retrieve_online_state(data, detail):
|
||||
return detail.is_online or detail.is_standby
|
||||
|
||||
|
||||
def _retrieve_motion_state(data, detail):
|
||||
def _retrieve_motion_state(data: AugustData, detail: DoorbellDetail) -> bool:
|
||||
latest = data.activity_stream.get_latest_device_activity(
|
||||
detail.device_id, {ActivityType.DOORBELL_MOTION}
|
||||
)
|
||||
@@ -47,7 +60,7 @@ def _retrieve_motion_state(data, detail):
|
||||
return _activity_time_based_state(latest)
|
||||
|
||||
|
||||
def _retrieve_ding_state(data, detail):
|
||||
def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail) -> bool:
|
||||
latest = data.activity_stream.get_latest_device_activity(
|
||||
detail.device_id, {ActivityType.DOORBELL_DING}
|
||||
)
|
||||
@@ -64,34 +77,62 @@ def _retrieve_ding_state(data, detail):
|
||||
return _activity_time_based_state(latest)
|
||||
|
||||
|
||||
def _activity_time_based_state(latest):
|
||||
def _activity_time_based_state(latest: Activity) -> bool:
|
||||
"""Get the latest state of the sensor."""
|
||||
start = latest.activity_start_time
|
||||
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
|
||||
return start <= _native_datetime() <= end
|
||||
|
||||
|
||||
def _native_datetime():
|
||||
def _native_datetime() -> datetime:
|
||||
"""Return time in the format august uses without timezone."""
|
||||
return datetime.now()
|
||||
|
||||
|
||||
SENSOR_NAME = 0
|
||||
SENSOR_DEVICE_CLASS = 1
|
||||
SENSOR_STATE_PROVIDER = 2
|
||||
SENSOR_STATE_IS_TIME_BASED = 3
|
||||
@dataclass
|
||||
class AugustRequiredKeysMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
# sensor_type: [name, device_class, state_provider, is_time_based]
|
||||
SENSOR_TYPES_DOORBELL = {
|
||||
"doorbell_ding": ["Ding", DEVICE_CLASS_OCCUPANCY, _retrieve_ding_state, True],
|
||||
"doorbell_motion": ["Motion", DEVICE_CLASS_MOTION, _retrieve_motion_state, True],
|
||||
"doorbell_online": [
|
||||
"Online",
|
||||
DEVICE_CLASS_CONNECTIVITY,
|
||||
_retrieve_online_state,
|
||||
False,
|
||||
],
|
||||
}
|
||||
value_fn: Callable[[AugustData, DoorbellDetail], bool]
|
||||
is_time_based: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class AugustBinarySensorEntityDescription(
|
||||
BinarySensorEntityDescription, AugustRequiredKeysMixin
|
||||
):
|
||||
"""Describes August binary_sensor entity."""
|
||||
|
||||
|
||||
SENSOR_TYPE_DOOR = BinarySensorEntityDescription(
|
||||
key="door_open",
|
||||
name="Open",
|
||||
)
|
||||
|
||||
|
||||
SENSOR_TYPES_DOORBELL: tuple[AugustBinarySensorEntityDescription, ...] = (
|
||||
AugustBinarySensorEntityDescription(
|
||||
key="doorbell_ding",
|
||||
name="Ding",
|
||||
device_class=DEVICE_CLASS_OCCUPANCY,
|
||||
value_fn=_retrieve_ding_state,
|
||||
is_time_based=True,
|
||||
),
|
||||
AugustBinarySensorEntityDescription(
|
||||
key="doorbell_motion",
|
||||
name="Motion",
|
||||
device_class=DEVICE_CLASS_MOTION,
|
||||
value_fn=_retrieve_motion_state,
|
||||
is_time_based=True,
|
||||
),
|
||||
AugustBinarySensorEntityDescription(
|
||||
key="doorbell_online",
|
||||
name="Online",
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
value_fn=_retrieve_online_state,
|
||||
is_time_based=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
@@ -109,16 +150,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
continue
|
||||
|
||||
_LOGGER.debug("Adding sensor class door for %s", door.device_name)
|
||||
entities.append(AugustDoorBinarySensor(data, "door_open", door))
|
||||
entities.append(AugustDoorBinarySensor(data, door, SENSOR_TYPE_DOOR))
|
||||
|
||||
for doorbell in data.doorbells:
|
||||
for sensor_type, sensor in SENSOR_TYPES_DOORBELL.items():
|
||||
for description in SENSOR_TYPES_DOORBELL:
|
||||
_LOGGER.debug(
|
||||
"Adding doorbell sensor class %s for %s",
|
||||
sensor[SENSOR_DEVICE_CLASS],
|
||||
description.device_class,
|
||||
doorbell.device_name,
|
||||
)
|
||||
entities.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell))
|
||||
entities.append(AugustDoorbellBinarySensor(data, doorbell, description))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -128,14 +169,16 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity):
|
||||
|
||||
_attr_device_class = DEVICE_CLASS_DOOR
|
||||
|
||||
def __init__(self, data, sensor_type, device):
|
||||
def __init__(self, data, device, description: BinarySensorEntityDescription):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(data, device)
|
||||
self.entity_description = description
|
||||
self._data = data
|
||||
self._sensor_type = sensor_type
|
||||
self._device = device
|
||||
self._attr_name = f"{device.device_name} Open"
|
||||
self._attr_unique_id = f"{self._device_id}_open"
|
||||
self._attr_name = f"{device.device_name} {description.name}"
|
||||
self._attr_unique_id = (
|
||||
f"{self._device_id}_{cast(str, description.name).lower()}"
|
||||
)
|
||||
self._update_from_data()
|
||||
|
||||
@callback
|
||||
@@ -164,41 +207,27 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity):
|
||||
class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity):
|
||||
"""Representation of an August binary sensor."""
|
||||
|
||||
def __init__(self, data, sensor_type, device):
|
||||
entity_description: AugustBinarySensorEntityDescription
|
||||
|
||||
def __init__(self, data, device, description: AugustBinarySensorEntityDescription):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(data, device)
|
||||
self.entity_description = description
|
||||
self._check_for_off_update_listener = None
|
||||
self._data = data
|
||||
self._sensor_type = sensor_type
|
||||
self._attr_device_class = self._sensor_config[SENSOR_DEVICE_CLASS]
|
||||
self._attr_name = f"{device.device_name} {self._sensor_config[SENSOR_NAME]}"
|
||||
self._attr_name = f"{device.device_name} {description.name}"
|
||||
self._attr_unique_id = (
|
||||
f"{self._device_id}_{self._sensor_config[SENSOR_NAME].lower()}"
|
||||
f"{self._device_id}_{cast(str, description.name).lower()}"
|
||||
)
|
||||
self._update_from_data()
|
||||
|
||||
@property
|
||||
def _sensor_config(self):
|
||||
"""Return the config for the sensor."""
|
||||
return SENSOR_TYPES_DOORBELL[self._sensor_type]
|
||||
|
||||
@property
|
||||
def _state_provider(self):
|
||||
"""Return the state provider for the binary sensor."""
|
||||
return self._sensor_config[SENSOR_STATE_PROVIDER]
|
||||
|
||||
@property
|
||||
def _is_time_based(self):
|
||||
"""Return true of false if the sensor is time based."""
|
||||
return self._sensor_config[SENSOR_STATE_IS_TIME_BASED]
|
||||
|
||||
@callback
|
||||
def _update_from_data(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
self._cancel_any_pending_updates()
|
||||
self._attr_is_on = self._state_provider(self._data, self._detail)
|
||||
self._attr_is_on = self.entity_description.value_fn(self._data, self._detail)
|
||||
|
||||
if self._is_time_based:
|
||||
if self.entity_description.is_time_based:
|
||||
self._attr_available = _retrieve_online_state(self._data, self._detail)
|
||||
self._schedule_update_to_recheck_turn_off_sensor()
|
||||
else:
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
"""Support for August sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from yalexs.activity import ActivityType
|
||||
from yalexs.keypad import KeypadDetail
|
||||
from yalexs.lock import LockDetail
|
||||
|
||||
from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, SensorEntity
|
||||
from homeassistant.components.august import AugustData
|
||||
from homeassistant.components.sensor import (
|
||||
DEVICE_CLASS_BATTERY,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE, STATE_UNAVAILABLE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity_registry import async_get_registry
|
||||
@@ -26,20 +38,44 @@ from .entity import AugustEntityMixin
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _retrieve_device_battery_state(detail):
|
||||
def _retrieve_device_battery_state(detail: LockDetail) -> int:
|
||||
"""Get the latest state of the sensor."""
|
||||
return detail.battery_level
|
||||
|
||||
|
||||
def _retrieve_linked_keypad_battery_state(detail):
|
||||
def _retrieve_linked_keypad_battery_state(detail: KeypadDetail) -> int | None:
|
||||
"""Get the latest state of the sensor."""
|
||||
return detail.battery_percentage
|
||||
|
||||
|
||||
SENSOR_TYPES_BATTERY = {
|
||||
"device_battery": {"state_provider": _retrieve_device_battery_state},
|
||||
"linked_keypad_battery": {"state_provider": _retrieve_linked_keypad_battery_state},
|
||||
}
|
||||
T = TypeVar("T", LockDetail, KeypadDetail)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AugustRequiredKeysMixin(Generic[T]):
|
||||
"""Mixin for required keys."""
|
||||
|
||||
value_fn: Callable[[T], int | None]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AugustSensorEntityDescription(
|
||||
SensorEntityDescription, AugustRequiredKeysMixin[T]
|
||||
):
|
||||
"""Describes August sensor entity."""
|
||||
|
||||
|
||||
SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail](
|
||||
key="device_battery",
|
||||
name="Battery",
|
||||
value_fn=_retrieve_device_battery_state,
|
||||
)
|
||||
|
||||
SENSOR_TYPE_KEYPAD_BATTERY = AugustSensorEntityDescription[KeypadDetail](
|
||||
key="linked_keypad_battery",
|
||||
name="Battery",
|
||||
value_fn=_retrieve_linked_keypad_battery_state,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
@@ -60,9 +96,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
operation_sensors.append(device)
|
||||
|
||||
for device in batteries["device_battery"]:
|
||||
state_provider = SENSOR_TYPES_BATTERY["device_battery"]["state_provider"]
|
||||
detail = data.get_device_detail(device.device_id)
|
||||
if detail is None or state_provider(detail) is None:
|
||||
if detail is None or SENSOR_TYPE_DEVICE_BATTERY.value_fn(detail) is None:
|
||||
_LOGGER.debug(
|
||||
"Not adding battery sensor for %s because it is not present",
|
||||
device.device_name,
|
||||
@@ -72,7 +107,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"Adding battery sensor for %s",
|
||||
device.device_name,
|
||||
)
|
||||
entities.append(AugustBatterySensor(data, "device_battery", device, device))
|
||||
entities.append(
|
||||
AugustBatterySensor[LockDetail](
|
||||
data, device, device, SENSOR_TYPE_DEVICE_BATTERY
|
||||
)
|
||||
)
|
||||
|
||||
for device in batteries["linked_keypad_battery"]:
|
||||
detail = data.get_device_detail(device.device_id)
|
||||
@@ -87,8 +126,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"Adding keypad battery sensor for %s",
|
||||
device.device_name,
|
||||
)
|
||||
keypad_battery_sensor = AugustBatterySensor(
|
||||
data, "linked_keypad_battery", detail.keypad, device
|
||||
keypad_battery_sensor = AugustBatterySensor[KeypadDetail](
|
||||
data, detail.keypad, device, SENSOR_TYPE_KEYPAD_BATTERY
|
||||
)
|
||||
entities.append(keypad_battery_sensor)
|
||||
migrate_unique_id_devices.append(keypad_battery_sensor)
|
||||
@@ -204,29 +243,35 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity):
|
||||
return f"{self._device_id}_lock_operator"
|
||||
|
||||
|
||||
class AugustBatterySensor(AugustEntityMixin, SensorEntity):
|
||||
class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[T]):
|
||||
"""Representation of an August sensor."""
|
||||
|
||||
entity_description: AugustSensorEntityDescription[T]
|
||||
_attr_device_class = DEVICE_CLASS_BATTERY
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
|
||||
def __init__(self, data, sensor_type, device, old_device):
|
||||
def __init__(
|
||||
self,
|
||||
data: AugustData,
|
||||
device,
|
||||
old_device,
|
||||
description: AugustSensorEntityDescription[T],
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(data, device)
|
||||
self._sensor_type = sensor_type
|
||||
self.entity_description = description
|
||||
self._old_device = old_device
|
||||
self._attr_name = f"{device.device_name} Battery"
|
||||
self._attr_unique_id = f"{self._device_id}_{sensor_type}"
|
||||
self._attr_name = f"{device.device_name} {description.name}"
|
||||
self._attr_unique_id = f"{self._device_id}_{description.key}"
|
||||
self._update_from_data()
|
||||
|
||||
@callback
|
||||
def _update_from_data(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
state_provider = SENSOR_TYPES_BATTERY[self._sensor_type]["state_provider"]
|
||||
self._attr_native_value = state_provider(self._detail)
|
||||
self._attr_native_value = self.entity_description.value_fn(self._detail)
|
||||
self._attr_available = self._attr_native_value is not None
|
||||
|
||||
@property
|
||||
def old_unique_id(self) -> str:
|
||||
"""Get the old unique id of the device sensor."""
|
||||
return f"{self._old_device.device_id}_{self._sensor_type}"
|
||||
return f"{self._old_device.device_id}_{self.entity_description.key}"
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer",
|
||||
"invalid_auth": "Authentification non valide",
|
||||
"cannot_connect": "\u00c9chec de connexion",
|
||||
"invalid_auth": "Authentification invalide",
|
||||
"unknown": "Erreur inattendue"
|
||||
},
|
||||
"step": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user