forked from home-assistant/core
Compare commits
524 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f3a1344b3 | |||
| 28f9d46f2e | |||
| 5156dd9c89 | |||
| a159f357ae | |||
| 8046684179 | |||
| 5a475ec7ea | |||
| 8c6edd8b81 | |||
| de496c693e | |||
| cb37d4d36a | |||
| 2aa82da615 | |||
| 04982f5e12 | |||
| b9e11b0f45 | |||
| 1e0d1c46ab | |||
| b5d499dda8 | |||
| d1615f9a6e | |||
| 516a3c0504 | |||
| 2a5c0d9b88 | |||
| a15a3c12d5 | |||
| a6131b3ebf | |||
| b9aadb252f | |||
| 1264c2cbfa | |||
| 716b559e5d | |||
| 30e4264aa9 | |||
| fb94f8ea18 | |||
| aea5760424 | |||
| debec3bfbc | |||
| 4122f94fb6 | |||
| b48a2cf2b5 | |||
| 0ca9ad1cc0 | |||
| ee555a3700 | |||
| a2bc3e3908 | |||
| 64b7f2c285 | |||
| db2435dc36 | |||
| 1d500fda67 | |||
| 558b0ec3b1 | |||
| 9780db1c22 | |||
| 5e39fb6da1 | |||
| 4450f919c3 | |||
| 3183bb78ff | |||
| e74f918382 | |||
| 247d2e7efd | |||
| 32b7edb608 | |||
| df4297be62 | |||
| 4c2e9fc759 | |||
| 2890fc7dd2 | |||
| 97be2c4ac9 | |||
| 762d284102 | |||
| 4967c287f8 | |||
| 5e463d6af4 | |||
| cbf4676ae4 | |||
| 81444c8f4a | |||
| 9861bd88b9 | |||
| b0f1c71129 | |||
| 86b845f04a | |||
| 3af0d6e484 | |||
| fca62f1ae8 | |||
| 4e8d68a2ef | |||
| 883ab44437 | |||
| abd17d9af9 | |||
| a906a1754e | |||
| 255beafe08 | |||
| e2679004a1 | |||
| 06bb692522 | |||
| 71599b8e75 | |||
| 79f8bea48d | |||
| 82b335a2c1 | |||
| 361d93eb96 | |||
| bab699eb0c | |||
| b8881ed85b | |||
| 4013b418dd | |||
| 80d714b865 | |||
| 7fcad580cb | |||
| 60b6ff4064 | |||
| 24252edf38 | |||
| 79aa7aacec | |||
| 92944fa509 | |||
| c0f0a4a1ac | |||
| a084b9fdde | |||
| 83b9b8b032 | |||
| bc47049d42 | |||
| 17360ede28 | |||
| f441f4d7c0 | |||
| 5ddc449247 | |||
| dd8d714c94 | |||
| c2079ddf6f | |||
| 5250590b17 | |||
| 93f4f14b2a | |||
| ba712ed514 | |||
| 6e76ca0fb3 | |||
| b0345cce68 | |||
| c4eddc8d11 | |||
| 7d89804a87 | |||
| b92f718e08 | |||
| ad0209a4a0 | |||
| d23d25c6b7 | |||
| c3abf5a190 | |||
| 621cf6ce58 | |||
| 83e0ed7b05 | |||
| 0752807aaf | |||
| 53df69ee6e | |||
| dbc38cdc6b | |||
| 102d55ec57 | |||
| 3f7cae8583 | |||
| a3a1d424c6 | |||
| 2cede8fec6 | |||
| 5816d495e3 | |||
| 24803b1e75 | |||
| 9732b8c0dd | |||
| 1626b3b7c9 | |||
| 8760a82dfa | |||
| 0f5d5ab0a2 | |||
| e05f7a9633 | |||
| 30656a4e72 | |||
| 5ccb9486e0 | |||
| a6d5891e8a | |||
| fc440f310b | |||
| 4d9ab42ab5 | |||
| e53f380710 | |||
| 6a514ac2de | |||
| 02bd8d67c8 | |||
| 2c118d4850 | |||
| 949225ffeb | |||
| 1ef04a8dde | |||
| 5c58f97e57 | |||
| f1b8c8855e | |||
| daf143f66e | |||
| 6c0e46f050 | |||
| 70133da025 | |||
| 837592381a | |||
| 101b073793 | |||
| df5f150531 | |||
| 4061314cd2 | |||
| 819be719ef | |||
| 80e4f19172 | |||
| 5b0ea21607 | |||
| 84634ce288 | |||
| fa1dc75517 | |||
| d8122d149b | |||
| 923300f4e7 | |||
| af66d0b647 | |||
| 8b9c4dadd0 | |||
| 857db679ae | |||
| 03ecd7f06c | |||
| c6bdee8dd8 | |||
| 800f403643 | |||
| 9b1c6b07f5 | |||
| f7c1a0c5e6 | |||
| 57a7c26c64 | |||
| d606e86b47 | |||
| f7a9319122 | |||
| b16151ac6d | |||
| bdd9099294 | |||
| d924f0b1d6 | |||
| 5dab9ba01b | |||
| ae118da5a1 | |||
| e24082be9a | |||
| 8fafbfaf82 | |||
| 6168fe006e | |||
| 6c633668f6 | |||
| 73a1dbffeb | |||
| 40217e764d | |||
| a7af0eaccd | |||
| 4ac29c6aef | |||
| ef023f084b | |||
| 441bca5bda | |||
| a8bee20aa3 | |||
| 04bea9c732 | |||
| 98cbc2a182 | |||
| 09518b1a71 | |||
| 42d22bb1a3 | |||
| 69c387a360 | |||
| 4b6fa12925 | |||
| c562cba030 | |||
| 40764b6995 | |||
| 4ee3290929 | |||
| dc02c37413 | |||
| 34becb541a | |||
| eabf88e3c9 | |||
| c3dac50f21 | |||
| f7240b52c5 | |||
| 2112b5a763 | |||
| 03b10b45c4 | |||
| 62361230f3 | |||
| 653306eb91 | |||
| 07e2cfb736 | |||
| f980434046 | |||
| 97084e9382 | |||
| 9db34fe232 | |||
| c4f0b4ab23 | |||
| 1647afc58a | |||
| 53ea8422f8 | |||
| 0b988b3fac | |||
| 5a4abe3ec1 | |||
| 89abc5ac69 | |||
| 08fe6653bb | |||
| 9aa18c7157 | |||
| cc7929f8fb | |||
| d657298791 | |||
| 05f393560f | |||
| 92da640d4c | |||
| ad3fd151aa | |||
| cd104dc08c | |||
| d3745d2519 | |||
| 931f3fa41a | |||
| 87b5a91212 | |||
| 3b8da62d84 | |||
| 86a48294f4 | |||
| a03884981f | |||
| ab695f90c7 | |||
| efcf8f9555 | |||
| f71903a563 | |||
| 95552e9a5b | |||
| 5da57271b2 | |||
| 62a7139f4d | |||
| a7be26cd95 | |||
| 9c3b0952e0 | |||
| c771f446b4 | |||
| 9a25561017 | |||
| bd870f0537 | |||
| d7f43bddfa | |||
| 87107c5a59 | |||
| 9ce920b35a | |||
| 15aff9662c | |||
| da6fb91886 | |||
| 1e880f7406 | |||
| 81153042d3 | |||
| 493ca261dc | |||
| 7493b340ca | |||
| e85e60ed6a | |||
| 8ff4d5dcbf | |||
| f2838e493b | |||
| a71edcf1a1 | |||
| 47bef74e7c | |||
| b757a7e3fe | |||
| 362ff5724d | |||
| 4f8363a5c2 | |||
| ae3925118c | |||
| b2fcab20a6 | |||
| 6423957d29 | |||
| 835cdad0a9 | |||
| d8d6decb38 | |||
| 16b42cc109 | |||
| a47f27821f | |||
| c797e7a973 | |||
| 245eb64405 | |||
| a895fcf057 | |||
| 5706fb26b8 | |||
| 3f82120cdc | |||
| 20df183470 | |||
| 980216795f | |||
| fdfcd841ba | |||
| 28a09794e9 | |||
| a0c9217375 | |||
| 469176c59b | |||
| 3ece672890 | |||
| c6ebba8843 | |||
| 1f047807a4 | |||
| f1b724c49a | |||
| 5ebed2046c | |||
| d1236a53b8 | |||
| 84f07ee992 | |||
| 360bffa3a9 | |||
| 2214d9b330 | |||
| 6a2d733d85 | |||
| 7392d5a30a | |||
| b3deeca939 | |||
| c38a3a239c | |||
| afa6ed09ef | |||
| deb966128f | |||
| 73707fa231 | |||
| 10ac39f6b2 | |||
| 2e05dc8618 | |||
| d8233b4de5 | |||
| 7cbc3ea65f | |||
| 6f0a9910ea | |||
| b8793760a1 | |||
| 6264f9c67b | |||
| 2a74deb84e | |||
| 9d1ff37a79 | |||
| 2f99164781 | |||
| 80ef32f09d | |||
| 63be0e2e1a | |||
| 74c4553bb0 | |||
| e240707b32 | |||
| 7c867852a9 | |||
| 2d149dc746 | |||
| 7edcddd3e4 | |||
| 71f658b560 | |||
| 9886db5d6d | |||
| c236cd070c | |||
| 9f1a830d32 | |||
| 1e69ce9111 | |||
| 389297155d | |||
| c341b86520 | |||
| 88eef379b2 | |||
| 34767d4058 | |||
| 12c3d54a63 | |||
| 33a185dade | |||
| c1c5776d85 | |||
| eda642554d | |||
| 51f5ce013f | |||
| f7794ea6b5 | |||
| 7a1bea7ff5 | |||
| c7c645776d | |||
| 667cb772e9 | |||
| 933d008e52 | |||
| d868f39aea | |||
| 28d776a0b0 | |||
| b5d541b596 | |||
| 4948499889 | |||
| 7696b101f6 | |||
| fd2987a9fd | |||
| 4c1d32020a | |||
| b40bdab0ae | |||
| d192aecd3b | |||
| d1781f5766 | |||
| 2c4461457a | |||
| 82959081de | |||
| acdac6d5e8 | |||
| d3d7889883 | |||
| 60ece3e1c9 | |||
| a9f8529460 | |||
| ec53b61f9e | |||
| e9f02edd8b | |||
| d1b7898219 | |||
| 8dc21ef619 | |||
| d9f91598a5 | |||
| c540acf2bd | |||
| f702f3efcd | |||
| 9410061405 | |||
| 485b28d9ea | |||
| d59200a9f5 | |||
| 44a92ca81c | |||
| d39fa39a03 | |||
| 36ec857523 | |||
| fcb8cdc146 | |||
| 2322b0b65f | |||
| 87baaf4255 | |||
| b7f0e877f0 | |||
| 5d92a04732 | |||
| 8ff879df22 | |||
| 9fb7ee676e | |||
| 2c855a3986 | |||
| cdd4894e30 | |||
| 5f26226712 | |||
| 8baf61031d | |||
| e90ba40553 | |||
| b38016425f | |||
| ee5e3f7691 | |||
| 7af6a4f493 | |||
| c25f26a290 | |||
| 8d62cb60a6 | |||
| 4f799069ea | |||
| af708b78e0 | |||
| f46e659740 | |||
| 7bd517e6ff | |||
| e9abdab1f5 | |||
| 86eee4f041 | |||
| 9db60c830c | |||
| c43a4682b9 | |||
| 2a4996055a | |||
| 4643fc2c14 | |||
| 6410b90d82 | |||
| e5c00eceae | |||
| fe65579df8 | |||
| 281beecb05 | |||
| 7546b5d269 | |||
| 490e3201b9 | |||
| 04be575139 | |||
| 854cae7f12 | |||
| 109d20978f | |||
| f8d284ec4b | |||
| 06ebe0810f | |||
| 802ad2ff51 | |||
| 9070a8d579 | |||
| e8b2a3de8b | |||
| 39549d5dd4 | |||
| 0c19e47bd4 | |||
| 05507d77e3 | |||
| 94558e2d40 | |||
| 4f22fe8f7f | |||
| 9e7dfbb857 | |||
| 02d182239a | |||
| 4e0f581747 | |||
| 42d97d348c | |||
| 69380c85ca | |||
| b38c647830 | |||
| 2396fd1090 | |||
| aa4eb89eee | |||
| 1b1bc6af95 | |||
| f17003a79c | |||
| ec70e8b0cd | |||
| d888c70ff0 | |||
| f29444002e | |||
| fc66997a36 | |||
| 35513ae072 | |||
| cd363d48c3 | |||
| d47ef835d7 | |||
| 00177c699e | |||
| 11b0086a01 | |||
| ceb177f80e | |||
| fa3832fbd7 | |||
| 2b9c903429 | |||
| a7c43f9b49 | |||
| b428196149 | |||
| e23da1a90f | |||
| 3951c2ea66 | |||
| fee152654d | |||
| 51073c948c | |||
| 91438088a0 | |||
| 427e1abdae | |||
| 6e7ac45ac0 | |||
| 4b3b9ebc29 | |||
| 649d8638ed | |||
| 12c4152dbe | |||
| 8f9572bb05 | |||
| 6d022ff4e0 | |||
| c0c2edb90a | |||
| b014219fdd | |||
| 216b8ef400 | |||
| f2ccd46267 | |||
| e16ba27ce8 | |||
| 506526a6a2 | |||
| a88678cf42 | |||
| d0b61af7ec | |||
| 04f5315ab2 | |||
| 7f9e4ba39e | |||
| 06aaf188ea | |||
| 627f994872 | |||
| 9e81ec5aae | |||
| 69753fca1d | |||
| 7773cc121e | |||
| 3aa56936ad | |||
| e66416c23d | |||
| a592feae3d | |||
| fc0d71e891 | |||
| d4640f1d24 | |||
| 6fe158836e | |||
| 629c0087f4 | |||
| 363bd75129 | |||
| 7592d350a8 | |||
| 8ac8401b4e | |||
| eed075dbfa | |||
| 23dbdedfb6 | |||
| 85ad29e28e | |||
| 35fc81b038 | |||
| 5d45b84cd2 | |||
| 7766649304 | |||
| 07e9020dfa | |||
| f504a759e0 | |||
| 9927de4801 | |||
| 1244fc4682 | |||
| e77a1b12f7 | |||
| 5459daaa10 | |||
| 400131df78 | |||
| 28e1843ff9 | |||
| df777318d1 | |||
| 6ad5e9e89c | |||
| a0bd8deee9 | |||
| 405cbd6a00 | |||
| 3e0eb5ab2c | |||
| fad75a70b6 | |||
| d9720283df | |||
| 14eed1778b | |||
| 049aaa7e8b | |||
| 35717e8216 | |||
| 2a081abc18 | |||
| b7f29c7358 | |||
| 3bb6373df5 | |||
| e1b4edec50 | |||
| 147bee57e1 | |||
| fcdaea64da | |||
| d1512d46be | |||
| 0be7db6270 | |||
| 2af0282725 | |||
| ff458c8417 | |||
| cc93152ff0 | |||
| 9965f01609 | |||
| e9c76ce694 | |||
| 58ab7d350d | |||
| e4d6e20ebd | |||
| 45e273897a | |||
| d9ec7142d7 | |||
| e162499267 | |||
| 67f21429e3 | |||
| a0563f06c9 | |||
| e7c4fdc8bb | |||
| c490e350bc | |||
| e11409ef99 | |||
| 5c8e415a76 | |||
| e795fb9497 | |||
| d0afabb85c | |||
| 4f3e8e9b94 | |||
| 46c1cbbc9c | |||
| 8d9a4ea278 | |||
| 22c83e2393 | |||
| c83a75f6f9 | |||
| 841c727112 | |||
| d8c9655bfd | |||
| 942ed89cc4 | |||
| a1fe6b9cf3 | |||
| 2567181cc2 | |||
| 028e4f6029 | |||
| b82e1a9bef | |||
| 438f226c31 | |||
| 2f139e3cb1 | |||
| 5d75e96fbf | |||
| dcf2ec5c37 | |||
| 2431e1ba98 | |||
| 4ead108c15 | |||
| ec8363fa49 | |||
| e7ff0a3f8b | |||
| f4c0eb4189 | |||
| b1ee5a76e1 | |||
| 6b9e8c301b | |||
| 89c3266c7e | |||
| cff0a632e8 | |||
| e04d8557ae | |||
| ca6286f241 | |||
| 35bcc9d5af | |||
| 25b45ce867 | |||
| d568209bd5 | |||
| 8a43e8af9e | |||
| 785e5b2c16 |
@@ -509,7 +509,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -522,7 +522,7 @@ jobs:
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -531,7 +531,7 @@ jobs:
|
||||
|
||||
- name: Generate artifact attestation
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
|
||||
uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0
|
||||
with:
|
||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
|
||||
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 12
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 9
|
||||
HA_SHORT_VERSION: "2025.5"
|
||||
HA_SHORT_VERSION: "2025.6"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||
# 10.3 is the oldest supported version
|
||||
|
||||
@@ -332,6 +332,7 @@ homeassistant.components.media_player.*
|
||||
homeassistant.components.media_source.*
|
||||
homeassistant.components.met_eireann.*
|
||||
homeassistant.components.metoffice.*
|
||||
homeassistant.components.miele.*
|
||||
homeassistant.components.mikrotik.*
|
||||
homeassistant.components.min_max.*
|
||||
homeassistant.components.minecraft_server.*
|
||||
@@ -463,6 +464,7 @@ homeassistant.components.slack.*
|
||||
homeassistant.components.sleepiq.*
|
||||
homeassistant.components.smhi.*
|
||||
homeassistant.components.smlight.*
|
||||
homeassistant.components.smtp.*
|
||||
homeassistant.components.snooz.*
|
||||
homeassistant.components.solarlog.*
|
||||
homeassistant.components.sonarr.*
|
||||
|
||||
Generated
+4
-3
@@ -1081,8 +1081,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/ombi/ @larssont
|
||||
/homeassistant/components/onboarding/ @home-assistant/core
|
||||
/tests/components/onboarding/ @home-assistant/core
|
||||
/homeassistant/components/oncue/ @bdraco @peterager
|
||||
/tests/components/oncue/ @bdraco @peterager
|
||||
/homeassistant/components/ondilo_ico/ @JeromeHXP
|
||||
/tests/components/ondilo_ico/ @JeromeHXP
|
||||
/homeassistant/components/onedrive/ @zweckj
|
||||
@@ -1260,6 +1258,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/recovery_mode/ @home-assistant/core
|
||||
/homeassistant/components/refoss/ @ashionky
|
||||
/tests/components/refoss/ @ashionky
|
||||
/homeassistant/components/rehlko/ @bdraco @peterager
|
||||
/tests/components/rehlko/ @bdraco @peterager
|
||||
/homeassistant/components/remote/ @home-assistant/core
|
||||
/tests/components/remote/ @home-assistant/core
|
||||
/homeassistant/components/remote_calendar/ @Thomas55555
|
||||
@@ -1474,7 +1474,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/steam_online/ @tkdrob
|
||||
/homeassistant/components/steamist/ @bdraco
|
||||
/tests/components/steamist/ @bdraco
|
||||
/homeassistant/components/stiebel_eltron/ @fucm
|
||||
/homeassistant/components/stiebel_eltron/ @fucm @ThyMYthOS
|
||||
/tests/components/stiebel_eltron/ @fucm @ThyMYthOS
|
||||
/homeassistant/components/stookwijzer/ @fwestenberg
|
||||
/tests/components/stookwijzer/ @fwestenberg
|
||||
/homeassistant/components/stream/ @hunterjm @uvjustin @allenporter
|
||||
|
||||
Generated
+1
-1
@@ -31,7 +31,7 @@ RUN \
|
||||
&& go2rtc --version
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.6.10
|
||||
RUN pip3 install uv==0.7.1
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"google_assistant_sdk",
|
||||
"google_cloud",
|
||||
"google_drive",
|
||||
"google_gemini",
|
||||
"google_generative_ai_conversation",
|
||||
"google_mail",
|
||||
"google_maps",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "nuki",
|
||||
"name": "Nuki",
|
||||
"integrations": ["nuki"],
|
||||
"iot_standards": ["matter"]
|
||||
}
|
||||
@@ -67,6 +67,7 @@ POLLEN_CATEGORY_MAP = {
|
||||
2: "moderate",
|
||||
3: "high",
|
||||
4: "very_high",
|
||||
5: "extreme",
|
||||
}
|
||||
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40)
|
||||
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"level": {
|
||||
"name": "Level",
|
||||
"state": {
|
||||
"extreme": "Extreme",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "Moderate",
|
||||
@@ -89,6 +90,7 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
@@ -123,6 +125,7 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
@@ -167,6 +170,7 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
@@ -181,6 +185,7 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
@@ -195,6 +200,7 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
|
||||
@@ -2,25 +2,38 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONNECTION_TYPE, LOCAL
|
||||
from .coordinator import AdaxCloudCoordinator, AdaxConfigEntry, AdaxLocalCoordinator
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AdaxConfigEntry) -> bool:
|
||||
"""Set up Adax from a config entry."""
|
||||
if entry.data.get(CONNECTION_TYPE) == LOCAL:
|
||||
local_coordinator = AdaxLocalCoordinator(hass, entry)
|
||||
entry.runtime_data = local_coordinator
|
||||
else:
|
||||
cloud_coordinator = AdaxCloudCoordinator(hass, entry)
|
||||
entry.runtime_data = cloud_coordinator
|
||||
|
||||
await entry.runtime_data.async_config_entry_first_refresh()
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AdaxConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: AdaxConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate old entry."""
|
||||
# convert title and unique_id to string
|
||||
if config_entry.version == 1:
|
||||
|
||||
@@ -12,57 +12,42 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
CONF_IP_ADDRESS,
|
||||
CONF_PASSWORD,
|
||||
CONF_TOKEN,
|
||||
CONF_UNIQUE_ID,
|
||||
PRECISION_WHOLE,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ACCOUNT_ID, CONNECTION_TYPE, DOMAIN, LOCAL
|
||||
from . import AdaxConfigEntry
|
||||
from .const import CONNECTION_TYPE, DOMAIN, LOCAL
|
||||
from .coordinator import AdaxCloudCoordinator, AdaxLocalCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: AdaxConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Adax thermostat with config flow."""
|
||||
if entry.data.get(CONNECTION_TYPE) == LOCAL:
|
||||
adax_data_handler = AdaxLocal(
|
||||
entry.data[CONF_IP_ADDRESS],
|
||||
entry.data[CONF_TOKEN],
|
||||
websession=async_get_clientsession(hass, verify_ssl=False),
|
||||
)
|
||||
local_coordinator = cast(AdaxLocalCoordinator, entry.runtime_data)
|
||||
async_add_entities(
|
||||
[LocalAdaxDevice(adax_data_handler, entry.data[CONF_UNIQUE_ID])], True
|
||||
[LocalAdaxDevice(local_coordinator, entry.data[CONF_UNIQUE_ID])],
|
||||
)
|
||||
else:
|
||||
cloud_coordinator = cast(AdaxCloudCoordinator, entry.runtime_data)
|
||||
async_add_entities(
|
||||
AdaxDevice(cloud_coordinator, device_id)
|
||||
for device_id in cloud_coordinator.data
|
||||
)
|
||||
return
|
||||
|
||||
adax_data_handler = Adax(
|
||||
entry.data[ACCOUNT_ID],
|
||||
entry.data[CONF_PASSWORD],
|
||||
websession=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
(
|
||||
AdaxDevice(room, adax_data_handler)
|
||||
for room in await adax_data_handler.get_rooms()
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class AdaxDevice(ClimateEntity):
|
||||
class AdaxDevice(CoordinatorEntity[AdaxCloudCoordinator], ClimateEntity):
|
||||
"""Representation of a heater."""
|
||||
|
||||
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
|
||||
@@ -76,20 +61,37 @@ class AdaxDevice(ClimateEntity):
|
||||
_attr_target_temperature_step = PRECISION_WHOLE
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
def __init__(self, heater_data: dict[str, Any], adax_data_handler: Adax) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AdaxCloudCoordinator,
|
||||
device_id: str,
|
||||
) -> None:
|
||||
"""Initialize the heater."""
|
||||
self._device_id = heater_data["id"]
|
||||
self._adax_data_handler = adax_data_handler
|
||||
super().__init__(coordinator)
|
||||
self._adax_data_handler: Adax = coordinator.adax_data_handler
|
||||
self._device_id = device_id
|
||||
|
||||
self._attr_unique_id = f"{heater_data['homeId']}_{heater_data['id']}"
|
||||
self._attr_name = self.room["name"]
|
||||
self._attr_unique_id = f"{self.room['homeId']}_{self._device_id}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, heater_data["id"])},
|
||||
identifiers={(DOMAIN, device_id)},
|
||||
# Instead of setting the device name to the entity name, adax
|
||||
# should be updated to set has_entity_name = True, and set the entity
|
||||
# name to None
|
||||
name=cast(str | None, self.name),
|
||||
manufacturer="Adax",
|
||||
)
|
||||
self._apply_data(self.room)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Whether the entity is available or not."""
|
||||
return super().available and self._device_id in self.coordinator.data
|
||||
|
||||
@property
|
||||
def room(self) -> dict[str, Any]:
|
||||
"""Gets the data for this particular device."""
|
||||
return self.coordinator.data[self._device_id]
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set hvac mode."""
|
||||
@@ -104,7 +106,9 @@ class AdaxDevice(ClimateEntity):
|
||||
)
|
||||
else:
|
||||
return
|
||||
await self._adax_data_handler.update()
|
||||
|
||||
# Request data refresh from source to verify that update was successful
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
@@ -114,28 +118,31 @@ class AdaxDevice(ClimateEntity):
|
||||
self._device_id, temperature, True
|
||||
)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data."""
|
||||
for room in await self._adax_data_handler.get_rooms():
|
||||
if room["id"] != self._device_id:
|
||||
continue
|
||||
self._attr_name = room["name"]
|
||||
self._attr_current_temperature = room.get("temperature")
|
||||
self._attr_target_temperature = room.get("targetTemperature")
|
||||
if room["heatingEnabled"]:
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
self._attr_icon = "mdi:radiator"
|
||||
else:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_icon = "mdi:radiator-off"
|
||||
return
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
if room := self.room:
|
||||
self._apply_data(room)
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
def _apply_data(self, room: dict[str, Any]) -> None:
|
||||
"""Update the appropriate attributues based on received data."""
|
||||
self._attr_current_temperature = room.get("temperature")
|
||||
self._attr_target_temperature = room.get("targetTemperature")
|
||||
if room["heatingEnabled"]:
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
self._attr_icon = "mdi:radiator"
|
||||
else:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_icon = "mdi:radiator-off"
|
||||
|
||||
|
||||
class LocalAdaxDevice(ClimateEntity):
|
||||
class LocalAdaxDevice(CoordinatorEntity[AdaxLocalCoordinator], ClimateEntity):
|
||||
"""Representation of a heater."""
|
||||
|
||||
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
|
||||
_attr_hvac_mode = HVACMode.HEAT
|
||||
_attr_hvac_mode = HVACMode.OFF
|
||||
_attr_icon = "mdi:radiator-off"
|
||||
_attr_max_temp = 35
|
||||
_attr_min_temp = 5
|
||||
_attr_supported_features = (
|
||||
@@ -146,9 +153,10 @@ class LocalAdaxDevice(ClimateEntity):
|
||||
_attr_target_temperature_step = PRECISION_WHOLE
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
def __init__(self, adax_data_handler: AdaxLocal, unique_id: str) -> None:
|
||||
def __init__(self, coordinator: AdaxLocalCoordinator, unique_id: str) -> None:
|
||||
"""Initialize the heater."""
|
||||
self._adax_data_handler = adax_data_handler
|
||||
super().__init__(coordinator)
|
||||
self._adax_data_handler: AdaxLocal = coordinator.adax_data_handler
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
@@ -169,17 +177,20 @@ class LocalAdaxDevice(ClimateEntity):
|
||||
return
|
||||
await self._adax_data_handler.set_target_temperature(temperature)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data."""
|
||||
data = await self._adax_data_handler.get_status()
|
||||
self._attr_current_temperature = data["current_temperature"]
|
||||
self._attr_available = self._attr_current_temperature is not None
|
||||
if (target_temp := data["target_temperature"]) == 0:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_icon = "mdi:radiator-off"
|
||||
if target_temp == 0:
|
||||
self._attr_target_temperature = self._attr_min_temp
|
||||
else:
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
self._attr_icon = "mdi:radiator"
|
||||
self._attr_target_temperature = target_temp
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
if data := self.coordinator.data:
|
||||
self._attr_current_temperature = data["current_temperature"]
|
||||
self._attr_available = self._attr_current_temperature is not None
|
||||
if (target_temp := data["target_temperature"]) == 0:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_icon = "mdi:radiator-off"
|
||||
if target_temp == 0:
|
||||
self._attr_target_temperature = self._attr_min_temp
|
||||
else:
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
self._attr_icon = "mdi:radiator"
|
||||
self._attr_target_temperature = target_temp
|
||||
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Constants for the Adax integration."""
|
||||
|
||||
import datetime
|
||||
from typing import Final
|
||||
|
||||
ACCOUNT_ID: Final = "account_id"
|
||||
@@ -9,3 +10,5 @@ DOMAIN: Final = "adax"
|
||||
LOCAL = "Local"
|
||||
WIFI_SSID = "wifi_ssid"
|
||||
WIFI_PSWD = "wifi_pswd"
|
||||
|
||||
SCAN_INTERVAL = datetime.timedelta(seconds=60)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""DataUpdateCoordinator for the Adax component."""
|
||||
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from adax import Adax
|
||||
from adax_local import Adax as AdaxLocal
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_TOKEN
|
||||
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 ACCOUNT_ID, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
type AdaxConfigEntry = ConfigEntry[AdaxCloudCoordinator | AdaxLocalCoordinator]
|
||||
|
||||
|
||||
class AdaxCloudCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
||||
"""Coordinator for updating data to and from Adax (cloud)."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: AdaxConfigEntry) -> None:
|
||||
"""Initialize the Adax coordinator used for Cloud mode."""
|
||||
super().__init__(
|
||||
hass,
|
||||
config_entry=entry,
|
||||
logger=_LOGGER,
|
||||
name="AdaxCloud",
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
self.adax_data_handler = Adax(
|
||||
entry.data[ACCOUNT_ID],
|
||||
entry.data[CONF_PASSWORD],
|
||||
websession=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
|
||||
"""Fetch data from the Adax."""
|
||||
rooms = await self.adax_data_handler.get_rooms() or []
|
||||
return {r["id"]: r for r in rooms}
|
||||
|
||||
|
||||
class AdaxLocalCoordinator(DataUpdateCoordinator[dict[str, Any] | None]):
|
||||
"""Coordinator for updating data to and from Adax (local)."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: AdaxConfigEntry) -> None:
|
||||
"""Initialize the Adax coordinator used for Local mode."""
|
||||
super().__init__(
|
||||
hass,
|
||||
config_entry=entry,
|
||||
logger=_LOGGER,
|
||||
name="AdaxLocal",
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
self.adax_data_handler = AdaxLocal(
|
||||
entry.data[CONF_IP_ADDRESS],
|
||||
entry.data[CONF_TOKEN],
|
||||
websession=async_get_clientsession(hass, verify_ssl=False),
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Fetch data from the Adax."""
|
||||
if result := await self.adax_data_handler.get_status():
|
||||
return cast(dict[str, Any], result)
|
||||
raise UpdateFailed("Got invalid status from device")
|
||||
@@ -93,7 +93,7 @@
|
||||
"name": "Internal temperature"
|
||||
},
|
||||
"last_self_test": {
|
||||
"name": "Last self test"
|
||||
"name": "Last self-test"
|
||||
},
|
||||
"last_transfer": {
|
||||
"name": "Last transfer"
|
||||
@@ -177,7 +177,7 @@
|
||||
"name": "Restore requirement"
|
||||
},
|
||||
"self_test_result": {
|
||||
"name": "Self test result"
|
||||
"name": "Self-test result"
|
||||
},
|
||||
"sensitivity": {
|
||||
"name": "Sensitivity"
|
||||
@@ -195,7 +195,7 @@
|
||||
"name": "Status"
|
||||
},
|
||||
"self_test_interval": {
|
||||
"name": "Self test interval"
|
||||
"name": "Self-test interval"
|
||||
},
|
||||
"time_left": {
|
||||
"name": "Time left"
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apsystems",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["apsystems-ez1==2.5.0"]
|
||||
"loggers": ["APsystemsEZ1"],
|
||||
"requirements": ["apsystems-ez1==2.6.0"]
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_MODE,
|
||||
ATTR_NAME,
|
||||
CONF_ACTIONS,
|
||||
CONF_ALIAS,
|
||||
CONF_CONDITIONS,
|
||||
CONF_DEVICE_ID,
|
||||
@@ -27,6 +28,7 @@ from homeassistant.const import (
|
||||
CONF_MODE,
|
||||
CONF_PATH,
|
||||
CONF_PLATFORM,
|
||||
CONF_TRIGGERS,
|
||||
CONF_VARIABLES,
|
||||
CONF_ZONE,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
@@ -86,11 +88,9 @@ from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .config import AutomationConfig, ValidationStatus
|
||||
from .const import (
|
||||
CONF_ACTIONS,
|
||||
CONF_INITIAL_STATE,
|
||||
CONF_TRACE,
|
||||
CONF_TRIGGER_VARIABLES,
|
||||
CONF_TRIGGERS,
|
||||
DEFAULT_INITIAL_STATE,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
|
||||
@@ -14,11 +14,15 @@ from homeassistant.components import blueprint
|
||||
from homeassistant.components.trace import TRACE_CONFIG_SCHEMA
|
||||
from homeassistant.config import config_per_platform, config_without_domain
|
||||
from homeassistant.const import (
|
||||
CONF_ACTION,
|
||||
CONF_ACTIONS,
|
||||
CONF_ALIAS,
|
||||
CONF_CONDITION,
|
||||
CONF_CONDITIONS,
|
||||
CONF_DESCRIPTION,
|
||||
CONF_ID,
|
||||
CONF_TRIGGER,
|
||||
CONF_TRIGGERS,
|
||||
CONF_VARIABLES,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -30,14 +34,10 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.yaml.input import UndefinedSubstitution
|
||||
|
||||
from .const import (
|
||||
CONF_ACTION,
|
||||
CONF_ACTIONS,
|
||||
CONF_HIDE_ENTITY,
|
||||
CONF_INITIAL_STATE,
|
||||
CONF_TRACE,
|
||||
CONF_TRIGGER,
|
||||
CONF_TRIGGER_VARIABLES,
|
||||
CONF_TRIGGERS,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
@@ -58,34 +58,9 @@ _MINIMAL_PLATFORM_SCHEMA = vol.Schema(
|
||||
def _backward_compat_schema(value: Any | None) -> Any:
|
||||
"""Backward compatibility for automations."""
|
||||
|
||||
if not isinstance(value, dict):
|
||||
return value
|
||||
|
||||
# `trigger` has been renamed to `triggers`
|
||||
if CONF_TRIGGER in value:
|
||||
if CONF_TRIGGERS in value:
|
||||
raise vol.Invalid(
|
||||
"Cannot specify both 'trigger' and 'triggers'. Please use 'triggers' only."
|
||||
)
|
||||
value[CONF_TRIGGERS] = value.pop(CONF_TRIGGER)
|
||||
|
||||
# `condition` has been renamed to `conditions`
|
||||
if CONF_CONDITION in value:
|
||||
if CONF_CONDITIONS in value:
|
||||
raise vol.Invalid(
|
||||
"Cannot specify both 'condition' and 'conditions'. Please use 'conditions' only."
|
||||
)
|
||||
value[CONF_CONDITIONS] = value.pop(CONF_CONDITION)
|
||||
|
||||
# `action` has been renamed to `actions`
|
||||
if CONF_ACTION in value:
|
||||
if CONF_ACTIONS in value:
|
||||
raise vol.Invalid(
|
||||
"Cannot specify both 'action' and 'actions'. Please use 'actions' only."
|
||||
)
|
||||
value[CONF_ACTIONS] = value.pop(CONF_ACTION)
|
||||
|
||||
return value
|
||||
value = cv.renamed(CONF_TRIGGER, CONF_TRIGGERS)(value)
|
||||
value = cv.renamed(CONF_ACTION, CONF_ACTIONS)(value)
|
||||
return cv.renamed(CONF_CONDITION, CONF_CONDITIONS)(value)
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
import logging
|
||||
|
||||
CONF_ACTION = "action"
|
||||
CONF_ACTIONS = "actions"
|
||||
CONF_TRIGGER = "trigger"
|
||||
CONF_TRIGGERS = "triggers"
|
||||
CONF_TRIGGER_VARIABLES = "trigger_variables"
|
||||
DOMAIN = "automation"
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ class BackupConfig:
|
||||
if agent_id not in self.data.agents:
|
||||
old_agent_retention = None
|
||||
self.data.agents[agent_id] = AgentConfig(
|
||||
protected=agent_config.get("protected", False),
|
||||
protected=agent_config.get("protected", True),
|
||||
retention=new_agent_retention,
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bluemaestro",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bluemaestro-ble==0.3.0"]
|
||||
"requirements": ["bluemaestro-ble==0.4.0"]
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
"bleak==0.22.3",
|
||||
"bleak-retry-connector==3.9.0",
|
||||
"bluetooth-adapters==0.21.4",
|
||||
"bluetooth-auto-recovery==1.4.5",
|
||||
"bluetooth-data-tools==1.28.0",
|
||||
"bluetooth-auto-recovery==1.5.1",
|
||||
"bluetooth-data-tools==1.28.1",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.44.0"
|
||||
"habluetooth==3.48.2"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError, ClientResponseError, ClientTimeout
|
||||
from bond_async import Bond, BPUPSubscriptions, start_bpup
|
||||
from bond_async import Bond, BPUPSubscriptions, RequestorUUID, start_bpup
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@@ -49,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BondConfigEntry) -> bool
|
||||
token=token,
|
||||
timeout=ClientTimeout(total=_API_TIMEOUT),
|
||||
session=async_get_clientsession(hass),
|
||||
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
|
||||
)
|
||||
hub = BondHub(bond, host)
|
||||
try:
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientConnectionError, ClientResponseError
|
||||
from bond_async import Bond
|
||||
from bond_async import Bond, RequestorUUID
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState, ConfigFlow, ConfigFlowResult
|
||||
@@ -34,7 +34,12 @@ TOKEN_SCHEMA = vol.Schema({})
|
||||
|
||||
async def async_get_token(hass: HomeAssistant, host: str) -> str | None:
|
||||
"""Try to fetch the token from the bond device."""
|
||||
bond = Bond(host, "", session=async_get_clientsession(hass))
|
||||
bond = Bond(
|
||||
host,
|
||||
"",
|
||||
session=async_get_clientsession(hass),
|
||||
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
|
||||
)
|
||||
response: dict[str, str] = {}
|
||||
with contextlib.suppress(ClientConnectionError):
|
||||
response = await bond.token()
|
||||
@@ -45,7 +50,10 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[st
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
bond = Bond(
|
||||
data[CONF_HOST], data[CONF_ACCESS_TOKEN], session=async_get_clientsession(hass)
|
||||
data[CONF_HOST],
|
||||
data[CONF_ACCESS_TOKEN],
|
||||
session=async_get_clientsession(hass),
|
||||
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
|
||||
)
|
||||
try:
|
||||
hub = BondHub(bond, data[CONF_HOST])
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
"known_hosts": "Add known host"
|
||||
},
|
||||
"data_description": {
|
||||
"known_hosts": "Hostnames or IP-addresses of cast devices, use if mDNS discovery is not working"
|
||||
"known_hosts": "Hostnames or IP addresses of cast devices, use if mDNS discovery is not working"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_known_hosts": "Known hosts must be a comma separated list of hosts."
|
||||
"invalid_known_hosts": "Known hosts must be a comma-separated list of hosts."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -418,9 +418,11 @@ class CloudTTSEntity(TextToSpeechEntity):
|
||||
language=language,
|
||||
voice=options.get(
|
||||
ATTR_VOICE,
|
||||
self._voice
|
||||
if language == self._language
|
||||
else DEFAULT_VOICES[language],
|
||||
(
|
||||
self._voice
|
||||
if language == self._language
|
||||
else DEFAULT_VOICES[language]
|
||||
),
|
||||
),
|
||||
gender=options.get(ATTR_GENDER),
|
||||
),
|
||||
@@ -435,6 +437,8 @@ class CloudTTSEntity(TextToSpeechEntity):
|
||||
class CloudProvider(Provider):
|
||||
"""Home Assistant Cloud speech API provider."""
|
||||
|
||||
has_entity = True
|
||||
|
||||
def __init__(self, cloud: Cloud[CloudClient]) -> None:
|
||||
"""Initialize cloud provider."""
|
||||
self.cloud = cloud
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.3.28"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.4.30"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from devolo_plc_api.device import Device
|
||||
from devolo_plc_api.exceptions.device import DeviceNotFound
|
||||
from devolo_plc_api.exceptions.device import DeviceNotFound, DevicePasswordProtected
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
@@ -22,7 +22,9 @@ from .const import DOMAIN, PRODUCT, SERIAL_NUMBER, TITLE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_IP_ADDRESS): str})
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_IP_ADDRESS): str, vol.Optional(CONF_PASSWORD): str}
|
||||
)
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Optional(CONF_PASSWORD): str})
|
||||
|
||||
|
||||
@@ -36,7 +38,16 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
|
||||
device = Device(data[CONF_IP_ADDRESS], zeroconf_instance=zeroconf_instance)
|
||||
|
||||
device.password = data[CONF_PASSWORD]
|
||||
|
||||
await device.async_connect(session_instance=async_client)
|
||||
|
||||
# Try a password protected, non-writing device API call that raises, if the password is wrong.
|
||||
# If only the plcnet API is available, we can continue without trying a password as the plcnet
|
||||
# API does not require a password.
|
||||
if device.device:
|
||||
await device.device.async_uptime()
|
||||
|
||||
await device.async_disconnect()
|
||||
|
||||
return {
|
||||
@@ -59,23 +70,22 @@ class DevoloHomeNetworkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the initial step."""
|
||||
errors: dict = {}
|
||||
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
except DeviceNotFound:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(info[SERIAL_NUMBER], raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
user_input[CONF_PASSWORD] = ""
|
||||
return self.async_create_entry(title=info[TITLE], data=user_input)
|
||||
if user_input is not None:
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
except DeviceNotFound:
|
||||
errors["base"] = "cannot_connect"
|
||||
except DevicePasswordProtected:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(
|
||||
info[SERIAL_NUMBER], raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=info[TITLE], data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
@@ -106,15 +116,27 @@ class DevoloHomeNetworkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by zeroconf."""
|
||||
title = self.context["title_placeholders"][CONF_NAME]
|
||||
errors: dict = {}
|
||||
data_schema: vol.Schema | None = None
|
||||
|
||||
if user_input is not None:
|
||||
data = {
|
||||
CONF_IP_ADDRESS: self.host,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_PASSWORD: user_input.get(CONF_PASSWORD, ""),
|
||||
}
|
||||
return self.async_create_entry(title=title, data=data)
|
||||
try:
|
||||
await validate_input(self.hass, data)
|
||||
except DevicePasswordProtected:
|
||||
errors = {"base": "invalid_auth"}
|
||||
data_schema = STEP_REAUTH_DATA_SCHEMA
|
||||
else:
|
||||
return self.async_create_entry(title=title, data=data)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="zeroconf_confirm",
|
||||
data_schema=data_schema,
|
||||
description_placeholders={"host_name": title},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
@@ -134,14 +156,21 @@ class DevoloHomeNetworkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by reauthentication."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=STEP_REAUTH_DATA_SCHEMA,
|
||||
)
|
||||
errors: dict = {}
|
||||
if user_input is not None:
|
||||
data = {
|
||||
CONF_IP_ADDRESS: self.host,
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
}
|
||||
try:
|
||||
await validate_input(self.hass, data)
|
||||
except DevicePasswordProtected:
|
||||
errors = {"base": "invalid_auth"}
|
||||
else:
|
||||
return self.async_update_reload_and_abort(self._reauth_entry, data=data)
|
||||
|
||||
data = {
|
||||
CONF_IP_ADDRESS: self.host,
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
}
|
||||
return self.async_update_reload_and_abort(self._reauth_entry, data=data)
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=STEP_REAUTH_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
"user": {
|
||||
"description": "[%key:common::config_flow::description::confirm_setup%]",
|
||||
"data": {
|
||||
"ip_address": "[%key:common::config_flow::data::ip%]"
|
||||
"ip_address": "[%key:common::config_flow::data::ip%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"ip_address": "IP address of your devolo Home Network device. This can be found in the devolo Home Network App on the device dashboard."
|
||||
"ip_address": "IP address of your devolo Home Network device. This can be found in the devolo Home Network App on the device dashboard.",
|
||||
"password": "Password you protected the device with."
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
@@ -16,16 +18,23 @@
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "Password you protected the device with."
|
||||
"password": "[%key:component::devolo_home_network::config::step::user::data_description::password%]"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"description": "Do you want to add the devolo home network device with the hostname `{host_name}` to Home Assistant?",
|
||||
"title": "Discovered devolo home network device"
|
||||
"title": "Discovered devolo home network device",
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "[%key:component::devolo_home_network::config::step::user::data_description::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/dnsip",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["aiodns==3.2.0"]
|
||||
"requirements": ["aiodns==3.3.0"]
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"events": "Comma separated list of events."
|
||||
"events": "Comma-separated list of events."
|
||||
},
|
||||
"data_description": {
|
||||
"events": "Add a comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion"
|
||||
"events": "Add a comma-separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from dataclasses import dataclass
|
||||
from typing import Generic
|
||||
|
||||
from deebot_client.capabilities import CapabilityEvent
|
||||
from deebot_client.events.water_info import WaterInfoEvent
|
||||
from deebot_client.events.water_info import MopAttachedEvent
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
@@ -32,9 +32,9 @@ class EcovacsBinarySensorEntityDescription(
|
||||
|
||||
|
||||
ENTITY_DESCRIPTIONS: tuple[EcovacsBinarySensorEntityDescription, ...] = (
|
||||
EcovacsBinarySensorEntityDescription[WaterInfoEvent](
|
||||
capability_fn=lambda caps: caps.water,
|
||||
value_fn=lambda e: e.mop_attached,
|
||||
EcovacsBinarySensorEntityDescription[MopAttachedEvent](
|
||||
capability_fn=lambda caps: caps.water.mop_attached if caps.water else None,
|
||||
value_fn=lambda e: e.value,
|
||||
key="water_mop_attached",
|
||||
translation_key="water_mop_attached",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""Ecovacs image entities."""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from deebot_client.capabilities import CapabilityMap
|
||||
from deebot_client.device import Device
|
||||
from deebot_client.events.map import CachedMapInfoEvent, MapChangedEvent
|
||||
from deebot_client.map import Map
|
||||
|
||||
from homeassistant.components.image import ImageEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -47,6 +50,7 @@ class EcovacsMap(
|
||||
"""Initialize entity."""
|
||||
super().__init__(device, capability, hass=hass)
|
||||
self._attr_extra_state_attributes = {}
|
||||
self._map = cast(Map, self._device.map)
|
||||
|
||||
entity_description = EntityDescription(
|
||||
key="map",
|
||||
@@ -55,7 +59,7 @@ class EcovacsMap(
|
||||
|
||||
def image(self) -> bytes | None:
|
||||
"""Return bytes of image or None."""
|
||||
if svg := self._device.map.get_svg_map():
|
||||
if svg := self._map.get_svg_map():
|
||||
return svg.encode()
|
||||
|
||||
return None
|
||||
@@ -80,4 +84,4 @@ class EcovacsMap(
|
||||
Only used by the generic entity update service.
|
||||
"""
|
||||
await super().async_update()
|
||||
self._device.map.refresh()
|
||||
self._map.refresh()
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.5.0"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==13.0.1"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ from typing import Any, Generic
|
||||
|
||||
from deebot_client.capabilities import CapabilitySetTypes
|
||||
from deebot_client.device import Device
|
||||
from deebot_client.events import WaterInfoEvent, WorkModeEvent
|
||||
from deebot_client.events import WorkModeEvent
|
||||
from deebot_client.events.water_info import WaterAmountEvent
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
@@ -31,9 +32,9 @@ class EcovacsSelectEntityDescription(
|
||||
|
||||
|
||||
ENTITY_DESCRIPTIONS: tuple[EcovacsSelectEntityDescription, ...] = (
|
||||
EcovacsSelectEntityDescription[WaterInfoEvent](
|
||||
capability_fn=lambda caps: caps.water,
|
||||
current_option_fn=lambda e: get_name_key(e.amount),
|
||||
EcovacsSelectEntityDescription[WaterAmountEvent](
|
||||
capability_fn=lambda caps: caps.water.amount if caps.water else None,
|
||||
current_option_fn=lambda e: get_name_key(e.value),
|
||||
options_fn=lambda water: [get_name_key(amount) for amount in water.types],
|
||||
key="water_amount",
|
||||
translation_key="water_amount",
|
||||
|
||||
@@ -9,7 +9,14 @@ from homeassistant.helpers.device_registry import DeviceEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.LIGHT, Platform.NUMBER, Platform.SENSOR]
|
||||
PLATFORMS = [
|
||||
Platform.CLIMATE,
|
||||
Platform.LIGHT,
|
||||
Platform.NUMBER,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.TIME,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -30,6 +30,22 @@
|
||||
"no_error": "mdi:check-circle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"filter_active": {
|
||||
"default": "mdi:pump",
|
||||
"state": {
|
||||
"off": "mdi:pump-off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"day_start_time": {
|
||||
"default": "mdi:weather-sunny"
|
||||
},
|
||||
"night_start_time": {
|
||||
"default": "mdi:moon-waning-crescent"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,14 @@
|
||||
"air_in_filter": "Air in filter"
|
||||
}
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"day_start_time": {
|
||||
"name": "Day start time"
|
||||
},
|
||||
"night_start_time": {
|
||||
"name": "Night start time"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""EHEIM Digital switches."""
|
||||
|
||||
from typing import Any, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
|
||||
from .entity import EheimDigitalEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: EheimDigitalConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the callbacks for the coordinator so switches can be added as devices are found."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
def async_setup_device_entities(
|
||||
device_address: dict[str, EheimDigitalDevice],
|
||||
) -> None:
|
||||
"""Set up the switch entities for one or multiple devices."""
|
||||
entities: list[SwitchEntity] = []
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalClassicVario):
|
||||
entities.append(EheimDigitalClassicVarioSwitch(coordinator, device)) # noqa: PERF401
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
coordinator.add_platform_callback(async_setup_device_entities)
|
||||
async_setup_device_entities(coordinator.hub.devices)
|
||||
|
||||
|
||||
class EheimDigitalClassicVarioSwitch(
|
||||
EheimDigitalEntity[EheimDigitalClassicVario], SwitchEntity
|
||||
):
|
||||
"""Represent an EHEIM Digital classicVARIO switch entity."""
|
||||
|
||||
_attr_translation_key = "filter_active"
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EheimDigitalUpdateCoordinator,
|
||||
device: EheimDigitalClassicVario,
|
||||
) -> None:
|
||||
"""Initialize an EHEIM Digital classicVARIO switch entity."""
|
||||
super().__init__(coordinator, device)
|
||||
self._attr_unique_id = device.mac_address
|
||||
self._async_update_attrs()
|
||||
|
||||
@override
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
await self._device.set_active(active=False)
|
||||
|
||||
@override
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
await self._device.set_active(active=True)
|
||||
|
||||
@override
|
||||
def _async_update_attrs(self) -> None:
|
||||
self._attr_is_on = self._device.is_active
|
||||
@@ -0,0 +1,132 @@
|
||||
"""EHEIM Digital time entities."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import time
|
||||
from typing import Generic, TypeVar, final, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.heater import EheimDigitalHeater
|
||||
|
||||
from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
|
||||
from .entity import EheimDigitalEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class EheimDigitalTimeDescription(TimeEntityDescription, Generic[_DeviceT_co]):
|
||||
"""Class describing EHEIM Digital time entities."""
|
||||
|
||||
value_fn: Callable[[_DeviceT_co], time | None]
|
||||
set_value_fn: Callable[[_DeviceT_co, time], Awaitable[None]]
|
||||
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
EheimDigitalTimeDescription[EheimDigitalClassicVario], ...
|
||||
] = (
|
||||
EheimDigitalTimeDescription[EheimDigitalClassicVario](
|
||||
key="day_start_time",
|
||||
translation_key="day_start_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda device: device.day_start_time,
|
||||
set_value_fn=lambda device, value: device.set_day_start_time(value),
|
||||
),
|
||||
EheimDigitalTimeDescription[EheimDigitalClassicVario](
|
||||
key="night_start_time",
|
||||
translation_key="night_start_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda device: device.night_start_time,
|
||||
set_value_fn=lambda device, value: device.set_night_start_time(value),
|
||||
),
|
||||
)
|
||||
|
||||
HEATER_DESCRIPTIONS: tuple[EheimDigitalTimeDescription[EheimDigitalHeater], ...] = (
|
||||
EheimDigitalTimeDescription[EheimDigitalHeater](
|
||||
key="day_start_time",
|
||||
translation_key="day_start_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda device: device.day_start_time,
|
||||
set_value_fn=lambda device, value: device.set_day_start_time(value),
|
||||
),
|
||||
EheimDigitalTimeDescription[EheimDigitalHeater](
|
||||
key="night_start_time",
|
||||
translation_key="night_start_time",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda device: device.night_start_time,
|
||||
set_value_fn=lambda device, value: device.set_night_start_time(value),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: EheimDigitalConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the callbacks for the coordinator so times can be added as devices are found."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
def async_setup_device_entities(
|
||||
device_address: dict[str, EheimDigitalDevice],
|
||||
) -> None:
|
||||
"""Set up the time entities for one or multiple devices."""
|
||||
entities: list[EheimDigitalTime[EheimDigitalDevice]] = []
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalClassicVario):
|
||||
entities.extend(
|
||||
EheimDigitalTime[EheimDigitalClassicVario](
|
||||
coordinator, device, description
|
||||
)
|
||||
for description in CLASSICVARIO_DESCRIPTIONS
|
||||
)
|
||||
if isinstance(device, EheimDigitalHeater):
|
||||
entities.extend(
|
||||
EheimDigitalTime[EheimDigitalHeater](
|
||||
coordinator, device, description
|
||||
)
|
||||
for description in HEATER_DESCRIPTIONS
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
coordinator.add_platform_callback(async_setup_device_entities)
|
||||
async_setup_device_entities(coordinator.hub.devices)
|
||||
|
||||
|
||||
@final
|
||||
class EheimDigitalTime(
|
||||
EheimDigitalEntity[_DeviceT_co], TimeEntity, Generic[_DeviceT_co]
|
||||
):
|
||||
"""Represent an EHEIM Digital time entity."""
|
||||
|
||||
entity_description: EheimDigitalTimeDescription[_DeviceT_co]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EheimDigitalUpdateCoordinator,
|
||||
device: _DeviceT_co,
|
||||
description: EheimDigitalTimeDescription[_DeviceT_co],
|
||||
) -> None:
|
||||
"""Initialize an EHEIM Digital time entity."""
|
||||
super().__init__(coordinator, device)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{device.mac_address}_{description.key}"
|
||||
|
||||
@override
|
||||
async def async_set_value(self, value: time) -> None:
|
||||
"""Change the time."""
|
||||
return await self.entity_description.set_value_fn(self._device, value)
|
||||
|
||||
@override
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update the entity attributes."""
|
||||
self._attr_native_value = self.entity_description.value_fn(self._device)
|
||||
@@ -9,12 +9,14 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from pyenphase import Envoy, EnvoyError, EnvoyTokenAuth
|
||||
from pyenphase.models.home import EnvoyInterfaceInformation
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -26,7 +28,7 @@ TOKEN_REFRESH_CHECK_INTERVAL = timedelta(days=1)
|
||||
STALE_TOKEN_THRESHOLD = timedelta(days=30).total_seconds()
|
||||
NOTIFICATION_ID = "enphase_envoy_notification"
|
||||
FIRMWARE_REFRESH_INTERVAL = timedelta(hours=4)
|
||||
|
||||
MAC_VERIFICATION_DELAY = timedelta(seconds=34)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -39,6 +41,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
envoy_serial_number: str
|
||||
envoy_firmware: str
|
||||
config_entry: EnphaseConfigEntry
|
||||
interface: EnvoyInterfaceInformation | None
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, envoy: Envoy, entry: EnphaseConfigEntry
|
||||
@@ -50,8 +53,10 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self.password = entry_data[CONF_PASSWORD]
|
||||
self._setup_complete = False
|
||||
self.envoy_firmware = ""
|
||||
self.interface = None
|
||||
self._cancel_token_refresh: CALLBACK_TYPE | None = None
|
||||
self._cancel_firmware_refresh: CALLBACK_TYPE | None = None
|
||||
self._cancel_mac_verification: CALLBACK_TYPE | None = None
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
@@ -121,6 +126,66 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
||||
)
|
||||
|
||||
def _schedule_mac_verification(
|
||||
self, delay: timedelta = MAC_VERIFICATION_DELAY
|
||||
) -> None:
|
||||
"""Schedule one time job to verify envoy mac address."""
|
||||
self.async_cancel_mac_verification()
|
||||
self._cancel_mac_verification = async_call_later(
|
||||
self.hass,
|
||||
delay,
|
||||
self._async_verify_mac,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_verify_mac(self, now: datetime.datetime) -> None:
|
||||
"""Verify Envoy active interface mac address in background."""
|
||||
self.hass.async_create_background_task(
|
||||
self._async_fetch_and_compare_mac(), "{name} verify envoy mac address"
|
||||
)
|
||||
|
||||
async def _async_fetch_and_compare_mac(self) -> None:
|
||||
"""Get Envoy interface information and update mac in device connections."""
|
||||
interface: (
|
||||
EnvoyInterfaceInformation | None
|
||||
) = await self.envoy.interface_settings()
|
||||
if interface is None:
|
||||
_LOGGER.debug("%s: interface information returned None", self.name)
|
||||
return
|
||||
# remember interface information so diagnostics can include in report
|
||||
self.interface = interface
|
||||
|
||||
# Add to or update device registry connections as needed
|
||||
device_registry = dr.async_get(self.hass)
|
||||
envoy_device = device_registry.async_get_device(
|
||||
identifiers={
|
||||
(
|
||||
DOMAIN,
|
||||
self.envoy_serial_number,
|
||||
)
|
||||
}
|
||||
)
|
||||
if envoy_device is None:
|
||||
_LOGGER.error(
|
||||
"No envoy device found in device registry: %s %s",
|
||||
DOMAIN,
|
||||
self.envoy_serial_number,
|
||||
)
|
||||
return
|
||||
|
||||
connection = (dr.CONNECTION_NETWORK_MAC, interface.mac)
|
||||
if connection in envoy_device.connections:
|
||||
_LOGGER.debug(
|
||||
"connection verified as existing: %s in %s", connection, self.name
|
||||
)
|
||||
return
|
||||
|
||||
device_registry.async_update_device(
|
||||
device_id=envoy_device.id,
|
||||
new_connections={connection},
|
||||
)
|
||||
_LOGGER.debug("added connection: %s to %s", connection, self.name)
|
||||
|
||||
@callback
|
||||
def _async_mark_setup_complete(self) -> None:
|
||||
"""Mark setup as complete and setup firmware checks and token refresh if needed."""
|
||||
@@ -132,6 +197,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
FIRMWARE_REFRESH_INTERVAL,
|
||||
cancel_on_shutdown=True,
|
||||
)
|
||||
self._schedule_mac_verification()
|
||||
self.async_cancel_token_refresh()
|
||||
if not isinstance(self.envoy.auth, EnvoyTokenAuth):
|
||||
return
|
||||
@@ -252,3 +318,10 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
if self._cancel_firmware_refresh:
|
||||
self._cancel_firmware_refresh()
|
||||
self._cancel_firmware_refresh = None
|
||||
|
||||
@callback
|
||||
def async_cancel_mac_verification(self) -> None:
|
||||
"""Cancel mac verification."""
|
||||
if self._cancel_mac_verification:
|
||||
self._cancel_mac_verification()
|
||||
self._cancel_mac_verification = None
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from attr import asdict
|
||||
@@ -63,6 +64,7 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
|
||||
"/ivp/ensemble/generator",
|
||||
"/ivp/meters",
|
||||
"/ivp/meters/readings",
|
||||
"/home,",
|
||||
]
|
||||
|
||||
for end_point in end_points:
|
||||
@@ -146,11 +148,25 @@ async def async_get_config_entry_diagnostics(
|
||||
"inverters": envoy_data.inverters,
|
||||
"tariff": envoy_data.tariff,
|
||||
}
|
||||
# Add Envoy active interface information to report
|
||||
active_interface: dict[str, Any] = {}
|
||||
if coordinator.interface:
|
||||
active_interface = {
|
||||
"name": (interface := coordinator.interface).primary_interface,
|
||||
"interface type": interface.interface_type,
|
||||
"mac": interface.mac,
|
||||
"uses dhcp": interface.dhcp,
|
||||
"firmware build date": datetime.fromtimestamp(
|
||||
interface.software_build_epoch
|
||||
).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"envoy timezone": interface.timezone,
|
||||
}
|
||||
|
||||
envoy_properties: dict[str, Any] = {
|
||||
"envoy_firmware": envoy.firmware,
|
||||
"part_number": envoy.part_number,
|
||||
"envoy_model": envoy.envoy_model,
|
||||
"active interface": active_interface,
|
||||
"supported_features": [feature.name for feature in envoy.supported_features],
|
||||
"phase_mode": envoy.phase_mode,
|
||||
"phase_count": envoy.phase_count,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "bronze",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==1.26.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
"storage_mode": {
|
||||
"name": "Storage mode",
|
||||
"state": {
|
||||
"self_consumption": "Self consumption",
|
||||
"self_consumption": "Self-consumption",
|
||||
"backup": "Full backup",
|
||||
"savings": "Savings mode"
|
||||
}
|
||||
@@ -393,7 +393,7 @@
|
||||
},
|
||||
"exceptions": {
|
||||
"unexpected_device": {
|
||||
"message": "Unexpected Envoy serial-number found at {host}; expected {expected_serial}, found {actual_serial}"
|
||||
"message": "Unexpected Envoy serial number found at {host}; expected {expected_serial}, found {actual_serial}"
|
||||
},
|
||||
"authentication_error": {
|
||||
"message": "Envoy authentication failure on {host}: {args}"
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eq3btsmart"],
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.14.0"]
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"]
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
SOURCE_REAUTH,
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigEntry,
|
||||
@@ -31,6 +32,7 @@ from homeassistant.config_entries import (
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||
@@ -302,7 +304,15 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
):
|
||||
return
|
||||
if entry.source == SOURCE_IGNORE:
|
||||
# Don't call _fetch_device_info() for ignored entries
|
||||
raise AbortFlow("already_configured")
|
||||
configured_host: str | None = entry.data.get(CONF_HOST)
|
||||
configured_port: int | None = entry.data.get(CONF_PORT)
|
||||
if configured_host == host and configured_port == port:
|
||||
# Don't probe to verify the mac is correct since
|
||||
# the host and port matches.
|
||||
raise AbortFlow("already_configured")
|
||||
configured_psk: str | None = entry.data.get(CONF_NOISE_PSK)
|
||||
await self._fetch_device_info(host, port or configured_port, configured_psk)
|
||||
updates: dict[str, Any] = {}
|
||||
|
||||
@@ -10,10 +10,18 @@ from homeassistant.const import CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import CONF_NOISE_PSK
|
||||
from .const import CONF_DEVICE_NAME
|
||||
from .dashboard import async_get_dashboard
|
||||
from .entry_data import ESPHomeConfigEntry
|
||||
|
||||
REDACT_KEYS = {CONF_NOISE_PSK, CONF_PASSWORD, "mac_address", "bluetooth_mac_address"}
|
||||
CONFIGURED_DEVICE_KEYS = (
|
||||
"configuration",
|
||||
"current_version",
|
||||
"deployed_version",
|
||||
"loaded_integrations",
|
||||
"target_platform",
|
||||
)
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
@@ -26,6 +34,9 @@ async def async_get_config_entry_diagnostics(
|
||||
|
||||
entry_data = config_entry.runtime_data
|
||||
device_info = entry_data.device_info
|
||||
device_name: str | None = (
|
||||
device_info.name if device_info else config_entry.data.get(CONF_DEVICE_NAME)
|
||||
)
|
||||
|
||||
if (storage_data := await entry_data.store.async_load()) is not None:
|
||||
diag["storage_data"] = storage_data
|
||||
@@ -45,7 +56,19 @@ async def async_get_config_entry_diagnostics(
|
||||
"scanner": await scanner.async_diagnostics(),
|
||||
}
|
||||
|
||||
diag_dashboard: dict[str, Any] = {"configured": False}
|
||||
diag["dashboard"] = diag_dashboard
|
||||
if dashboard := async_get_dashboard(hass):
|
||||
diag["dashboard"] = dashboard.addon_slug
|
||||
diag_dashboard["configured"] = True
|
||||
diag_dashboard["supports_update"] = dashboard.supports_update
|
||||
diag_dashboard["last_update_success"] = dashboard.last_update_success
|
||||
diag_dashboard["last_exception"] = dashboard.last_exception
|
||||
diag_dashboard["addon"] = dashboard.addon_slug
|
||||
if device_name and dashboard.data:
|
||||
diag_dashboard["has_matching_name"] = device_name in dashboard.data
|
||||
if data := dashboard.data.get(device_name):
|
||||
diag_dashboard["device"] = {
|
||||
key: data.get(key) for key in CONFIGURED_DEVICE_KEYS
|
||||
}
|
||||
|
||||
return async_redact_data(diag, REDACT_KEYS)
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==30.0.1",
|
||||
"aioesphomeapi==30.1.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.14.0"
|
||||
"bleak-esphome==2.15.1"
|
||||
],
|
||||
"zeroconf": ["_esphomelib._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -195,7 +195,10 @@
|
||||
"message": "Error compiling {configuration}; Try again in ESPHome dashboard for more information."
|
||||
},
|
||||
"error_uploading": {
|
||||
"message": "Error during OTA of {configuration}; Try again in ESPHome dashboard for more information."
|
||||
"message": "Error during OTA (Over-The-Air) of {configuration}; Try again in ESPHome dashboard for more information."
|
||||
},
|
||||
"ota_in_progress": {
|
||||
"message": "An OTA (Over-The-Air) update is already in progress for {configuration}."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ from homeassistant.util.enum import try_parse_enum
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ESPHomeDashboardCoordinator
|
||||
from .dashboard import async_get_dashboard
|
||||
from .domain_data import DomainData
|
||||
from .entity import (
|
||||
EsphomeEntity,
|
||||
convert_api_error_ha_error,
|
||||
@@ -62,7 +61,7 @@ async def async_setup_entry(
|
||||
|
||||
if (dashboard := async_get_dashboard(hass)) is None:
|
||||
return
|
||||
entry_data = DomainData.get(hass).get_entry_data(entry)
|
||||
entry_data = entry.runtime_data
|
||||
assert entry_data.device_info is not None
|
||||
device_name = entry_data.device_info.name
|
||||
unsubs: list[CALLBACK_TYPE] = []
|
||||
@@ -126,21 +125,17 @@ class ESPHomeDashboardUpdateEntity(
|
||||
(dr.CONNECTION_NETWORK_MAC, entry_data.device_info.mac_address)
|
||||
}
|
||||
)
|
||||
self._install_lock = asyncio.Lock()
|
||||
self._available_future: asyncio.Future[None] | None = None
|
||||
self._update_attrs()
|
||||
|
||||
@callback
|
||||
def _update_attrs(self) -> None:
|
||||
"""Update the supported features."""
|
||||
# If the device has deep sleep, we can't assume we can install updates
|
||||
# as the ESP will not be connectable (by design).
|
||||
coordinator = self.coordinator
|
||||
device_info = self._device_info
|
||||
# Install support can change at run time
|
||||
if (
|
||||
coordinator.last_update_success
|
||||
and coordinator.supports_update
|
||||
and not device_info.has_deep_sleep
|
||||
):
|
||||
if coordinator.last_update_success and coordinator.supports_update:
|
||||
self._attr_supported_features = UpdateEntityFeature.INSTALL
|
||||
else:
|
||||
self._attr_supported_features = NO_FEATURES
|
||||
@@ -179,6 +174,13 @@ class ESPHomeDashboardUpdateEntity(
|
||||
self, static_info: list[EntityInfo] | None = None
|
||||
) -> None:
|
||||
"""Handle updated data from the device."""
|
||||
if (
|
||||
self._entry_data.available
|
||||
and self._available_future
|
||||
and not self._available_future.done()
|
||||
):
|
||||
self._available_future.set_result(None)
|
||||
self._available_future = None
|
||||
self._update_attrs()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -193,17 +195,46 @@ class ESPHomeDashboardUpdateEntity(
|
||||
entry_data.async_subscribe_device_updated(self._handle_device_update)
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Handle entity about to be removed from Home Assistant."""
|
||||
if self._available_future and not self._available_future.done():
|
||||
self._available_future.cancel()
|
||||
self._available_future = None
|
||||
|
||||
async def _async_wait_available(self) -> None:
|
||||
"""Wait until the device is available."""
|
||||
# If the device has deep sleep, we need to wait for it to wake up
|
||||
# and connect to the network to be able to install the update.
|
||||
if self._entry_data.available:
|
||||
return
|
||||
self._available_future = self.hass.loop.create_future()
|
||||
try:
|
||||
await self._available_future
|
||||
finally:
|
||||
self._available_future = None
|
||||
|
||||
async def async_install(
|
||||
self, version: str | None, backup: bool, **kwargs: Any
|
||||
) -> None:
|
||||
"""Install an update."""
|
||||
async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()):
|
||||
coordinator = self.coordinator
|
||||
api = coordinator.api
|
||||
device = coordinator.data.get(self._device_info.name)
|
||||
assert device is not None
|
||||
configuration = device["configuration"]
|
||||
try:
|
||||
if self._install_lock.locked():
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="ota_in_progress",
|
||||
translation_placeholders={
|
||||
"configuration": self._device_info.name,
|
||||
},
|
||||
)
|
||||
|
||||
# Ensure only one OTA per device at a time
|
||||
async with self._install_lock:
|
||||
# Ensure only one compile at a time for ALL devices
|
||||
async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()):
|
||||
coordinator = self.coordinator
|
||||
api = coordinator.api
|
||||
device = coordinator.data.get(self._device_info.name)
|
||||
assert device is not None
|
||||
configuration = device["configuration"]
|
||||
if not await api.compile(configuration):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
@@ -212,14 +243,25 @@ class ESPHomeDashboardUpdateEntity(
|
||||
"configuration": configuration,
|
||||
},
|
||||
)
|
||||
if not await api.upload(configuration, "OTA"):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="error_uploading",
|
||||
translation_placeholders={
|
||||
"configuration": configuration,
|
||||
},
|
||||
)
|
||||
|
||||
# If the device uses deep sleep, there's a small chance it goes
|
||||
# to sleep right after the dashboard connects but before the OTA
|
||||
# starts. In that case, the update won't go through, so we try
|
||||
# again to catch it on its next wakeup.
|
||||
attempts = 2 if self._device_info.has_deep_sleep else 1
|
||||
try:
|
||||
for attempt in range(1, attempts + 1):
|
||||
await self._async_wait_available()
|
||||
if await api.upload(configuration, "OTA"):
|
||||
break
|
||||
if attempt == attempts:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="error_uploading",
|
||||
translation_placeholders={
|
||||
"configuration": configuration,
|
||||
},
|
||||
)
|
||||
finally:
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_FEATURE_DEVICE_TRACKING,
|
||||
DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
|
||||
DEFAULT_SSL,
|
||||
DOMAIN,
|
||||
FRITZ_AUTH_EXCEPTIONS,
|
||||
@@ -38,6 +40,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> bool:
|
||||
"""Set up fritzboxtools from config entry."""
|
||||
_LOGGER.debug("Setting up FRITZ!Box Tools component")
|
||||
|
||||
avm_wrapper = AvmWrapper(
|
||||
hass=hass,
|
||||
config_entry=entry,
|
||||
@@ -46,6 +49,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> boo
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
use_tls=entry.data.get(CONF_SSL, DEFAULT_SSL),
|
||||
device_discovery_enabled=entry.options.get(
|
||||
CONF_FEATURE_DEVICE_TRACKING, DEFAULT_CONF_FEATURE_DEVICE_TRACKING
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -62,6 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> boo
|
||||
raise ConfigEntryAuthFailed("Missing UPnP configuration")
|
||||
|
||||
await avm_wrapper.async_config_entry_first_refresh()
|
||||
await avm_wrapper.async_trigger_cleanup()
|
||||
|
||||
entry.runtime_data = avm_wrapper
|
||||
|
||||
|
||||
@@ -35,7 +35,9 @@ from homeassistant.helpers.service_info.ssdp import (
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from .const import (
|
||||
CONF_FEATURE_DEVICE_TRACKING,
|
||||
CONF_OLD_DISCOVERY,
|
||||
DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
|
||||
DEFAULT_CONF_OLD_DISCOVERY,
|
||||
DEFAULT_HOST,
|
||||
DEFAULT_HTTP_PORT,
|
||||
@@ -72,7 +74,8 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Initialize FRITZ!Box Tools flow."""
|
||||
self._name: str = ""
|
||||
self._password: str = ""
|
||||
self._use_tls: bool = False
|
||||
self._use_tls: bool = DEFAULT_SSL
|
||||
self._feature_device_discovery: bool = DEFAULT_CONF_FEATURE_DEVICE_TRACKING
|
||||
self._port: int | None = None
|
||||
self._username: str = ""
|
||||
self._model: str = ""
|
||||
@@ -141,6 +144,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
options={
|
||||
CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(),
|
||||
CONF_OLD_DISCOVERY: DEFAULT_CONF_OLD_DISCOVERY,
|
||||
CONF_FEATURE_DEVICE_TRACKING: self._feature_device_discovery,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -204,6 +208,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._username = user_input[CONF_USERNAME]
|
||||
self._password = user_input[CONF_PASSWORD]
|
||||
self._use_tls = user_input[CONF_SSL]
|
||||
self._feature_device_discovery = user_input[CONF_FEATURE_DEVICE_TRACKING]
|
||||
self._port = self._determine_port(user_input)
|
||||
|
||||
error = await self.async_fritz_tools_init()
|
||||
@@ -234,6 +239,10 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
|
||||
vol.Required(
|
||||
CONF_FEATURE_DEVICE_TRACKING,
|
||||
default=DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
|
||||
): bool,
|
||||
}
|
||||
),
|
||||
errors=errors or {},
|
||||
@@ -250,6 +259,10 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
|
||||
vol.Required(
|
||||
CONF_FEATURE_DEVICE_TRACKING,
|
||||
default=DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
|
||||
): bool,
|
||||
}
|
||||
),
|
||||
description_placeholders={"name": self._name},
|
||||
@@ -405,7 +418,7 @@ class FritzBoxToolsOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle options flow."""
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
return self.async_create_entry(data=user_input)
|
||||
|
||||
options = self.config_entry.options
|
||||
data_schema = vol.Schema(
|
||||
@@ -420,6 +433,13 @@ class FritzBoxToolsOptionsFlowHandler(OptionsFlow):
|
||||
CONF_OLD_DISCOVERY,
|
||||
default=options.get(CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY),
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_FEATURE_DEVICE_TRACKING,
|
||||
default=options.get(
|
||||
CONF_FEATURE_DEVICE_TRACKING,
|
||||
DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
|
||||
),
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
return self.async_show_form(step_id="init", data_schema=data_schema)
|
||||
|
||||
@@ -40,6 +40,9 @@ PLATFORMS = [
|
||||
CONF_OLD_DISCOVERY = "old_discovery"
|
||||
DEFAULT_CONF_OLD_DISCOVERY = False
|
||||
|
||||
CONF_FEATURE_DEVICE_TRACKING = "feature_device_tracking"
|
||||
DEFAULT_CONF_FEATURE_DEVICE_TRACKING = True
|
||||
|
||||
DSL_CONNECTION: Literal["dsl"] = "dsl"
|
||||
|
||||
DEFAULT_DEVICE_NAME = "Unknown device"
|
||||
|
||||
@@ -39,6 +39,7 @@ from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import (
|
||||
CONF_OLD_DISCOVERY,
|
||||
DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
|
||||
DEFAULT_CONF_OLD_DISCOVERY,
|
||||
DEFAULT_HOST,
|
||||
DEFAULT_SSL,
|
||||
@@ -175,6 +176,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
username: str = DEFAULT_USERNAME,
|
||||
host: str = DEFAULT_HOST,
|
||||
use_tls: bool = DEFAULT_SSL,
|
||||
device_discovery_enabled: bool = DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
|
||||
) -> None:
|
||||
"""Initialize FritzboxTools class."""
|
||||
super().__init__(
|
||||
@@ -202,6 +204,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
self.port = port
|
||||
self.username = username
|
||||
self.use_tls = use_tls
|
||||
self.device_discovery_enabled = device_discovery_enabled
|
||||
self.has_call_deflections: bool = False
|
||||
self._model: str | None = None
|
||||
self._current_firmware: str | None = None
|
||||
@@ -332,10 +335,15 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
"entity_states": {},
|
||||
}
|
||||
try:
|
||||
await self.async_scan_devices()
|
||||
await self.async_update_device_info()
|
||||
|
||||
if self.device_discovery_enabled:
|
||||
await self.async_scan_devices()
|
||||
|
||||
entity_data["entity_states"] = await self.hass.async_add_executor_job(
|
||||
self._entity_states_update
|
||||
)
|
||||
|
||||
if self.has_call_deflections:
|
||||
entity_data[
|
||||
"call_deflections"
|
||||
@@ -521,7 +529,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
return {}
|
||||
|
||||
def manage_device_info(
|
||||
self, dev_info: Device, dev_mac: str, consider_home: bool
|
||||
self, dev_info: Device, dev_mac: str, consider_home: float
|
||||
) -> bool:
|
||||
"""Update device lists and return if device is new."""
|
||||
_LOGGER.debug("Client dev_info: %s", dev_info)
|
||||
@@ -551,12 +559,8 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
if new_device:
|
||||
async_dispatcher_send(self.hass, self.signal_device_new)
|
||||
|
||||
async def async_scan_devices(self, now: datetime | None = None) -> None:
|
||||
"""Scan for new devices and return a list of found device ids."""
|
||||
|
||||
if self.hass.is_stopping:
|
||||
_ha_is_stopping("scan devices")
|
||||
return
|
||||
async def async_update_device_info(self, now: datetime | None = None) -> None:
|
||||
"""Update own device information."""
|
||||
|
||||
_LOGGER.debug("Checking host info for FRITZ!Box device %s", self.host)
|
||||
(
|
||||
@@ -565,6 +569,13 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
self._release_url,
|
||||
) = await self._async_update_device_info()
|
||||
|
||||
async def async_scan_devices(self, now: datetime | None = None) -> None:
|
||||
"""Scan for new network devices."""
|
||||
|
||||
if self.hass.is_stopping:
|
||||
_ha_is_stopping("scan devices")
|
||||
return
|
||||
|
||||
_LOGGER.debug("Checking devices for FRITZ!Box device %s", self.host)
|
||||
_default_consider_home = DEFAULT_CONSIDER_HOME.total_seconds()
|
||||
if self._options:
|
||||
@@ -683,7 +694,10 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
|
||||
async def async_trigger_cleanup(self) -> None:
|
||||
"""Trigger device trackers cleanup."""
|
||||
device_hosts = await self._async_update_hosts_info()
|
||||
_LOGGER.debug("Device tracker cleanup triggered")
|
||||
device_hosts = {self.mac: Device(True, "", "", "", "", None)}
|
||||
if self.device_discovery_enabled:
|
||||
device_hosts = await self._async_update_hosts_info()
|
||||
entity_reg: er.EntityRegistry = er.async_get(self.hass)
|
||||
config_entry = self.config_entry
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"data_description_port": "Leave empty to use the default port.",
|
||||
"data_description_username": "Username for the FRITZ!Box.",
|
||||
"data_description_password": "Password for the FRITZ!Box.",
|
||||
"data_description_ssl": "Use SSL to connect to the FRITZ!Box."
|
||||
"data_description_ssl": "Use SSL to connect to the FRITZ!Box.",
|
||||
"data_description_feature_device_tracking": "Enable or disable the network device tracking feature.",
|
||||
"data_feature_device_tracking": "Enable network device tracking"
|
||||
},
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
@@ -15,12 +17,14 @@
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]"
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"feature_device_tracking": "[%key:component::fritz::common::data_feature_device_tracking%]"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "[%key:component::fritz::common::data_description_username%]",
|
||||
"password": "[%key:component::fritz::common::data_description_password%]",
|
||||
"ssl": "[%key:component::fritz::common::data_description_ssl%]"
|
||||
"ssl": "[%key:component::fritz::common::data_description_ssl%]",
|
||||
"feature_device_tracking": "[%key:component::fritz::common::data_description_feature_device_tracking%]"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
@@ -57,14 +61,16 @@
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]"
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"feature_device_tracking": "[%key:component::fritz::common::data_feature_device_tracking%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::fritz::common::data_description_host%]",
|
||||
"port": "[%key:component::fritz::common::data_description_port%]",
|
||||
"username": "[%key:component::fritz::common::data_description_username%]",
|
||||
"password": "[%key:component::fritz::common::data_description_password%]",
|
||||
"ssl": "[%key:component::fritz::common::data_description_ssl%]"
|
||||
"ssl": "[%key:component::fritz::common::data_description_ssl%]",
|
||||
"feature_device_tracking": "[%key:component::fritz::common::data_description_feature_device_tracking%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -89,11 +95,13 @@
|
||||
"init": {
|
||||
"data": {
|
||||
"consider_home": "Seconds to consider a device at 'home'",
|
||||
"old_discovery": "Enable old discovery method"
|
||||
"old_discovery": "Enable old discovery method",
|
||||
"feature_device_tracking": "[%key:component::fritz::common::data_feature_device_tracking%]"
|
||||
},
|
||||
"data_description": {
|
||||
"consider_home": "Time in seconds to consider a device at home. Default is 180 seconds.",
|
||||
"old_discovery": "Enable old discovery method. This is needed for some scenarios."
|
||||
"old_discovery": "Enable old discovery method. This is needed for some scenarios.",
|
||||
"feature_device_tracking": "[%key:component::fritz::common::data_description_feature_device_tracking%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,32 @@ BINARY_SENSOR_TYPES: Final[tuple[FritzBinarySensorEntityDescription, ...]] = (
|
||||
suitable=lambda device: device.device_lock is not None,
|
||||
is_on=lambda device: not device.device_lock,
|
||||
),
|
||||
FritzBinarySensorEntityDescription(
|
||||
key="battery_low",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
suitable=lambda device: device.battery_low is not None,
|
||||
is_on=lambda device: device.battery_low,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
FritzBinarySensorEntityDescription(
|
||||
key="holiday_active",
|
||||
translation_key="holiday_active",
|
||||
suitable=lambda device: device.holiday_active is not None,
|
||||
is_on=lambda device: device.holiday_active,
|
||||
),
|
||||
FritzBinarySensorEntityDescription(
|
||||
key="summer_active",
|
||||
translation_key="summer_active",
|
||||
suitable=lambda device: device.summer_active is not None,
|
||||
is_on=lambda device: device.summer_active,
|
||||
),
|
||||
FritzBinarySensorEntityDescription(
|
||||
key="window_open",
|
||||
translation_key="window_open",
|
||||
suitable=lambda device: device.window_open is not None,
|
||||
is_on=lambda device: device.window_open,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -144,6 +144,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
self.check_active_or_lock_mode()
|
||||
if kwargs.get(ATTR_HVAC_MODE) is HVACMode.OFF:
|
||||
await self.async_set_hkr_state("off")
|
||||
elif (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
||||
@@ -168,11 +169,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new operation mode."""
|
||||
if self.data.holiday_active or self.data.summer_active:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="change_hvac_while_active_mode",
|
||||
)
|
||||
self.check_active_or_lock_mode()
|
||||
if self.hvac_mode is hvac_mode:
|
||||
LOGGER.debug(
|
||||
"%s is already in requested hvac mode %s", self.name, hvac_mode
|
||||
@@ -204,16 +201,13 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set preset mode."""
|
||||
if self.data.holiday_active or self.data.summer_active:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="change_preset_while_active_mode",
|
||||
)
|
||||
self.check_active_or_lock_mode()
|
||||
await self.async_set_hkr_state(PRESET_API_HKR_STATE_MAPPING[preset_mode])
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> ClimateExtraAttributes:
|
||||
"""Return the device specific state attributes."""
|
||||
# deprecated with #143394, can be removed in 2025.11
|
||||
attrs: ClimateExtraAttributes = {
|
||||
ATTR_STATE_BATTERY_LOW: self.data.battery_low,
|
||||
}
|
||||
@@ -229,3 +223,17 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
attrs[ATTR_STATE_WINDOW_OPEN] = self.data.window_open
|
||||
|
||||
return attrs
|
||||
|
||||
def check_active_or_lock_mode(self) -> None:
|
||||
"""Check if in summer/vacation mode or lock enabled."""
|
||||
if self.data.holiday_active or self.data.summer_active:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="change_settings_while_active_mode",
|
||||
)
|
||||
|
||||
if self.data.lock:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="change_settings_while_lock_enabled",
|
||||
)
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"holiday_active": {
|
||||
"default": "mdi:bag-suitcase-outline",
|
||||
"state": {
|
||||
"on": "mdi:bag-suitcase-outline",
|
||||
"off": "mdi:bag-suitcase-off-outline"
|
||||
}
|
||||
},
|
||||
"summer_active": {
|
||||
"default": "mdi:radiator-off",
|
||||
"state": {
|
||||
"on": "mdi:radiator-off",
|
||||
"off": "mdi:radiator"
|
||||
}
|
||||
},
|
||||
"window_open": {
|
||||
"default": "mdi:window-open",
|
||||
"state": {
|
||||
"on": "mdi:window-open",
|
||||
"off": "mdi:window-closed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"thermostat": {
|
||||
"state_attributes": {
|
||||
|
||||
@@ -55,7 +55,10 @@
|
||||
"binary_sensor": {
|
||||
"alarm": { "name": "Alarm" },
|
||||
"device_lock": { "name": "Button lock via UI" },
|
||||
"lock": { "name": "Button lock on device" }
|
||||
"holiday_active": { "name": "Holiday mode" },
|
||||
"lock": { "name": "Button lock on device" },
|
||||
"summer_active": { "name": "Summer mode" },
|
||||
"window_open": { "name": "Open window detected" }
|
||||
},
|
||||
"climate": {
|
||||
"thermostat": {
|
||||
@@ -85,11 +88,11 @@
|
||||
"manual_switching_disabled": {
|
||||
"message": "Can't toggle switch while manual switching is disabled for the device."
|
||||
},
|
||||
"change_preset_while_active_mode": {
|
||||
"message": "Can't change preset while holiday or summer mode is active on the device."
|
||||
"change_settings_while_lock_enabled": {
|
||||
"message": "Can't change settings while manual access for telephone, app, or user interface is disabled on the device"
|
||||
},
|
||||
"change_hvac_while_active_mode": {
|
||||
"message": "Can't change HVAC mode while holiday or summer mode is active on the device."
|
||||
"change_settings_while_active_mode": {
|
||||
"message": "Can't change settings while holiday or summer mode is active on the device."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Configure Prefixes",
|
||||
"title": "Configure prefixes",
|
||||
"data": {
|
||||
"prefixes": "Prefixes (comma separated list)"
|
||||
"prefixes": "Prefixes (comma-separated list)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -82,13 +82,13 @@
|
||||
"ac_frequency_too_high": "AC frequency too high",
|
||||
"ac_frequency_too_low": "AC frequency too low",
|
||||
"ac_grid_outside_permissible_limits": "AC grid outside the permissible limits",
|
||||
"stand_alone_operation_detected": "Stand alone operation detected",
|
||||
"stand_alone_operation_detected": "Stand-alone operation detected",
|
||||
"rcmu_error": "RCMU error",
|
||||
"arc_detection_triggered": "Arc detection triggered",
|
||||
"overcurrent_ac": "Overcurrent (AC)",
|
||||
"overcurrent_dc": "Overcurrent (DC)",
|
||||
"dc_module_over_temperature": "DC module over temperature",
|
||||
"ac_module_over_temperature": "AC module over temperature",
|
||||
"dc_module_over_temperature": "DC module overtemperature",
|
||||
"ac_module_over_temperature": "AC module overtemperature",
|
||||
"no_power_fed_in_despite_closed_relay": "No power being fed in, despite closed relay",
|
||||
"pv_output_too_low_for_feeding_energy_into_the_grid": "PV output too low for feeding energy into the grid",
|
||||
"low_pv_voltage_dc_input_voltage_too_low": "Low PV voltage - DC input voltage too low for feeding energy into the grid",
|
||||
@@ -133,16 +133,16 @@
|
||||
"no_energy_fed_by_mppt1_past_24_hours": "No energy fed into the grid by MPPT1 in the past 24 hours",
|
||||
"dc_low_string_1": "DC low string 1",
|
||||
"dc_low_string_2": "DC low string 2",
|
||||
"derating_caused_by_over_frequency": "Derating caused by over-frequency",
|
||||
"derating_caused_by_over_frequency": "Derating caused by overfrequency",
|
||||
"arc_detector_switched_off": "Arc detector switched off (e.g. during external arc monitoring)",
|
||||
"grid_voltage_dependent_power_reduction_active": "Grid Voltage Dependent Power Reduction is active",
|
||||
"grid_voltage_dependent_power_reduction_active": "Grid voltage-dependent power reduction (GVDPR) is active",
|
||||
"can_bus_full": "CAN bus is full",
|
||||
"ac_module_temperature_sensor_faulty_l3": "AC module temperature sensor faulty (L3)",
|
||||
"dc_module_temperature_sensor_faulty": "DC module temperature sensor faulty",
|
||||
"internal_processor_status": "Warning about the internal processor status. See status code for more information",
|
||||
"eeprom_reinitialised": "EEPROM has been re-initialised",
|
||||
"initialisation_error_usb_flash_drive_not_supported": "Initialisation error – USB flash drive is not supported",
|
||||
"initialisation_error_usb_stick_over_current": "Initialisation error – Over current on USB stick",
|
||||
"initialisation_error_usb_stick_over_current": "Initialisation error – Overcurrent on USB stick",
|
||||
"no_usb_flash_drive_connected": "No USB flash drive connected",
|
||||
"update_file_not_recognised_or_missing": "Update file not recognised or not present",
|
||||
"update_file_does_not_match_device": "Update file does not match the device, update file too old",
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250411.0"]
|
||||
"requirements": ["home-assistant-frontend==20250502.0"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/garages_amsterdam",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["odp-amsterdam==6.0.2"]
|
||||
"requirements": ["odp-amsterdam==6.1.1"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Virtual integration: Google Gemini."""
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "google_gemini",
|
||||
"name": "Google Gemini",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "google_generative_ai_conversation"
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
"""The google_travel_time component."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import CONF_TIME
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Google Maps Travel Time from a config entry."""
|
||||
@@ -16,3 +23,37 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate an old config entry."""
|
||||
|
||||
if config_entry.version == 1:
|
||||
_LOGGER.debug(
|
||||
"Migrating from version %s.%s",
|
||||
config_entry.version,
|
||||
config_entry.minor_version,
|
||||
)
|
||||
options = dict(config_entry.options)
|
||||
if options.get(CONF_TIME) == "now":
|
||||
options[CONF_TIME] = None
|
||||
elif options.get(CONF_TIME) is not None:
|
||||
if dt_util.parse_time(options[CONF_TIME]) is None:
|
||||
try:
|
||||
from_timestamp = dt_util.utc_from_timestamp(int(options[CONF_TIME]))
|
||||
options[CONF_TIME] = (
|
||||
f"{from_timestamp.time().hour:02}:{from_timestamp.time().minute:02}"
|
||||
)
|
||||
except ValueError:
|
||||
_LOGGER.error(
|
||||
"Invalid time format found while migrating: %s. The old config never worked. Reset to default (empty)",
|
||||
options[CONF_TIME],
|
||||
)
|
||||
options[CONF_TIME] = None
|
||||
hass.config_entries.async_update_entry(config_entry, options=options, version=2)
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s.%s successful",
|
||||
config_entry.version,
|
||||
config_entry.minor_version,
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -19,6 +19,7 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
TimeSelector,
|
||||
)
|
||||
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
|
||||
|
||||
@@ -106,7 +107,7 @@ OPTIONS_SCHEMA = vol.Schema(
|
||||
translation_key=CONF_TIME_TYPE,
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_TIME, default=""): cv.string,
|
||||
vol.Optional(CONF_TIME): TimeSelector(),
|
||||
vol.Optional(CONF_TRAFFIC_MODEL): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=TRAFFIC_MODELS,
|
||||
@@ -181,8 +182,7 @@ async def validate_input(
|
||||
) -> dict[str, str] | None:
|
||||
"""Validate the user input allows us to connect."""
|
||||
try:
|
||||
await hass.async_add_executor_job(
|
||||
validate_config_entry,
|
||||
await validate_config_entry(
|
||||
hass,
|
||||
user_input[CONF_API_KEY],
|
||||
user_input[CONF_ORIGIN],
|
||||
@@ -201,7 +201,7 @@ async def validate_input(
|
||||
class GoogleTravelTimeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Google Maps Travel Time."""
|
||||
|
||||
VERSION = 1
|
||||
VERSION = 2
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
"""Constants for Google Travel Time."""
|
||||
|
||||
from google.maps.routing_v2 import (
|
||||
RouteTravelMode,
|
||||
TrafficModel,
|
||||
TransitPreferences,
|
||||
Units,
|
||||
)
|
||||
|
||||
DOMAIN = "google_travel_time"
|
||||
|
||||
ATTRIBUTION = "Powered by Google"
|
||||
@@ -7,7 +14,6 @@ ATTRIBUTION = "Powered by Google"
|
||||
CONF_DESTINATION = "destination"
|
||||
CONF_OPTIONS = "options"
|
||||
CONF_ORIGIN = "origin"
|
||||
CONF_TRAVEL_MODE = "travel_mode"
|
||||
CONF_AVOID = "avoid"
|
||||
CONF_UNITS = "units"
|
||||
CONF_ARRIVAL_TIME = "arrival_time"
|
||||
@@ -79,11 +85,37 @@ ALL_LANGUAGES = [
|
||||
|
||||
AVOID_OPTIONS = ["tolls", "highways", "ferries", "indoor"]
|
||||
TRANSIT_PREFS = ["less_walking", "fewer_transfers"]
|
||||
TRANSIT_PREFS_TO_GOOGLE_SDK_ENUM = {
|
||||
"less_walking": TransitPreferences.TransitRoutingPreference.LESS_WALKING,
|
||||
"fewer_transfers": TransitPreferences.TransitRoutingPreference.FEWER_TRANSFERS,
|
||||
}
|
||||
TRANSPORT_TYPES = ["bus", "subway", "train", "tram", "rail"]
|
||||
TRANSPORT_TYPES_TO_GOOGLE_SDK_ENUM = {
|
||||
"bus": TransitPreferences.TransitTravelMode.BUS,
|
||||
"subway": TransitPreferences.TransitTravelMode.SUBWAY,
|
||||
"train": TransitPreferences.TransitTravelMode.TRAIN,
|
||||
"tram": TransitPreferences.TransitTravelMode.LIGHT_RAIL,
|
||||
"rail": TransitPreferences.TransitTravelMode.RAIL,
|
||||
}
|
||||
TRAVEL_MODES = ["driving", "walking", "bicycling", "transit"]
|
||||
TRAVEL_MODES_TO_GOOGLE_SDK_ENUM = {
|
||||
"driving": RouteTravelMode.DRIVE,
|
||||
"walking": RouteTravelMode.WALK,
|
||||
"bicycling": RouteTravelMode.BICYCLE,
|
||||
"transit": RouteTravelMode.TRANSIT,
|
||||
}
|
||||
TRAFFIC_MODELS = ["best_guess", "pessimistic", "optimistic"]
|
||||
TRAFFIC_MODELS_TO_GOOGLE_SDK_ENUM = {
|
||||
"best_guess": TrafficModel.BEST_GUESS,
|
||||
"pessimistic": TrafficModel.PESSIMISTIC,
|
||||
"optimistic": TrafficModel.OPTIMISTIC,
|
||||
}
|
||||
|
||||
# googlemaps library uses "metric" or "imperial" terminology in distance_matrix
|
||||
UNITS_METRIC = "metric"
|
||||
UNITS_IMPERIAL = "imperial"
|
||||
UNITS = [UNITS_METRIC, UNITS_IMPERIAL]
|
||||
UNITS_TO_GOOGLE_SDK_ENUM = {
|
||||
UNITS_METRIC: Units.METRIC,
|
||||
UNITS_IMPERIAL: Units.IMPERIAL,
|
||||
}
|
||||
|
||||
@@ -2,41 +2,80 @@
|
||||
|
||||
import logging
|
||||
|
||||
from googlemaps import Client
|
||||
from googlemaps.distance_matrix import distance_matrix
|
||||
from googlemaps.exceptions import ApiError, Timeout, TransportError
|
||||
from google.api_core.client_options import ClientOptions
|
||||
from google.api_core.exceptions import (
|
||||
Forbidden,
|
||||
GatewayTimeout,
|
||||
GoogleAPIError,
|
||||
Unauthorized,
|
||||
)
|
||||
from google.maps.routing_v2 import (
|
||||
ComputeRoutesRequest,
|
||||
Location,
|
||||
RoutesAsyncClient,
|
||||
RouteTravelMode,
|
||||
Waypoint,
|
||||
)
|
||||
from google.type import latlng_pb2
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.location import find_coordinates
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def validate_config_entry(
|
||||
def convert_to_waypoint(hass: HomeAssistant, location: str) -> Waypoint | None:
|
||||
"""Convert a location to a Waypoint.
|
||||
|
||||
Will either use coordinates or if none are found, use the location as an address.
|
||||
"""
|
||||
coordinates = find_coordinates(hass, location)
|
||||
if coordinates is None:
|
||||
return None
|
||||
try:
|
||||
formatted_coordinates = coordinates.split(",")
|
||||
vol.Schema(cv.gps(formatted_coordinates))
|
||||
except (AttributeError, vol.ExactSequenceInvalid):
|
||||
return Waypoint(address=location)
|
||||
return Waypoint(
|
||||
location=Location(
|
||||
lat_lng=latlng_pb2.LatLng(
|
||||
latitude=float(formatted_coordinates[0]),
|
||||
longitude=float(formatted_coordinates[1]),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def validate_config_entry(
|
||||
hass: HomeAssistant, api_key: str, origin: str, destination: str
|
||||
) -> None:
|
||||
"""Return whether the config entry data is valid."""
|
||||
resolved_origin = find_coordinates(hass, origin)
|
||||
resolved_destination = find_coordinates(hass, destination)
|
||||
resolved_origin = convert_to_waypoint(hass, origin)
|
||||
resolved_destination = convert_to_waypoint(hass, destination)
|
||||
client_options = ClientOptions(api_key=api_key)
|
||||
client = RoutesAsyncClient(client_options=client_options)
|
||||
field_mask = "routes.duration"
|
||||
request = ComputeRoutesRequest(
|
||||
origin=resolved_origin,
|
||||
destination=resolved_destination,
|
||||
travel_mode=RouteTravelMode.DRIVE,
|
||||
)
|
||||
try:
|
||||
client = Client(api_key, timeout=10)
|
||||
except ValueError as value_error:
|
||||
_LOGGER.error("Malformed API key")
|
||||
raise InvalidApiKeyException from value_error
|
||||
try:
|
||||
distance_matrix(client, resolved_origin, resolved_destination, mode="driving")
|
||||
except ApiError as api_error:
|
||||
if api_error.status == "REQUEST_DENIED":
|
||||
_LOGGER.error("Request denied: %s", api_error.message)
|
||||
raise InvalidApiKeyException from api_error
|
||||
_LOGGER.error("Unknown error: %s", api_error.message)
|
||||
raise UnknownException from api_error
|
||||
except TransportError as transport_error:
|
||||
_LOGGER.error("Unknown error: %s", transport_error)
|
||||
raise UnknownException from transport_error
|
||||
except Timeout as timeout_error:
|
||||
await client.compute_routes(
|
||||
request, metadata=[("x-goog-fieldmask", field_mask)]
|
||||
)
|
||||
except (Unauthorized, Forbidden) as unauthorized_error:
|
||||
_LOGGER.error("Request denied: %s", unauthorized_error.message)
|
||||
raise InvalidApiKeyException from unauthorized_error
|
||||
except GatewayTimeout as timeout_error:
|
||||
_LOGGER.error("Timeout error")
|
||||
raise TimeoutError from timeout_error
|
||||
except GoogleAPIError as unknown_error:
|
||||
_LOGGER.error("Unknown error: %s", unknown_error)
|
||||
raise UnknownException from unknown_error
|
||||
|
||||
|
||||
class InvalidApiKeyException(Exception):
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/google_travel_time",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googlemaps", "homeassistant.helpers.location"],
|
||||
"requirements": ["googlemaps==2.5.1"]
|
||||
"loggers": ["google", "homeassistant.helpers.location"],
|
||||
"requirements": ["google-maps-routing==0.6.14"]
|
||||
}
|
||||
|
||||
@@ -2,12 +2,22 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import datetime
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from googlemaps import Client
|
||||
from googlemaps.distance_matrix import distance_matrix
|
||||
from googlemaps.exceptions import ApiError, Timeout, TransportError
|
||||
from google.api_core.client_options import ClientOptions
|
||||
from google.api_core.exceptions import GoogleAPIError
|
||||
from google.maps.routing_v2 import (
|
||||
ComputeRoutesRequest,
|
||||
Route,
|
||||
RouteModifiers,
|
||||
RoutesAsyncClient,
|
||||
RouteTravelMode,
|
||||
RoutingPreference,
|
||||
TransitPreferences,
|
||||
)
|
||||
from google.protobuf import timestamp_pb2
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@@ -17,6 +27,8 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_LANGUAGE,
|
||||
CONF_MODE,
|
||||
CONF_NAME,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
UnitOfTime,
|
||||
@@ -30,26 +42,49 @@ from homeassistant.util import dt as dt_util
|
||||
from .const import (
|
||||
ATTRIBUTION,
|
||||
CONF_ARRIVAL_TIME,
|
||||
CONF_AVOID,
|
||||
CONF_DEPARTURE_TIME,
|
||||
CONF_DESTINATION,
|
||||
CONF_ORIGIN,
|
||||
CONF_TRAFFIC_MODEL,
|
||||
CONF_TRANSIT_MODE,
|
||||
CONF_TRANSIT_ROUTING_PREFERENCE,
|
||||
CONF_UNITS,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
TRAFFIC_MODELS_TO_GOOGLE_SDK_ENUM,
|
||||
TRANSIT_PREFS_TO_GOOGLE_SDK_ENUM,
|
||||
TRANSPORT_TYPES_TO_GOOGLE_SDK_ENUM,
|
||||
TRAVEL_MODES_TO_GOOGLE_SDK_ENUM,
|
||||
UNITS_TO_GOOGLE_SDK_ENUM,
|
||||
)
|
||||
from .helpers import convert_to_waypoint
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
SCAN_INTERVAL = datetime.timedelta(minutes=10)
|
||||
FIELD_MASK = "routes.duration,routes.localized_values"
|
||||
|
||||
|
||||
def convert_time_to_utc(timestr):
|
||||
"""Take a string like 08:00:00 and convert it to a unix timestamp."""
|
||||
combined = datetime.combine(
|
||||
dt_util.start_of_local_day(), dt_util.parse_time(timestr)
|
||||
def convert_time(time_str: str) -> timestamp_pb2.Timestamp | None:
|
||||
"""Convert a string like '08:00' to a google pb2 Timestamp.
|
||||
|
||||
If the time is in the past, it will be shifted to the next day.
|
||||
"""
|
||||
parsed_time = dt_util.parse_time(time_str)
|
||||
if TYPE_CHECKING:
|
||||
assert parsed_time is not None
|
||||
start_of_day = dt_util.start_of_local_day()
|
||||
combined = datetime.datetime.combine(
|
||||
start_of_day,
|
||||
parsed_time,
|
||||
start_of_day.tzinfo,
|
||||
)
|
||||
if combined < datetime.now():
|
||||
combined = combined + timedelta(days=1)
|
||||
return dt_util.as_timestamp(combined)
|
||||
if combined < dt_util.now():
|
||||
combined = combined + datetime.timedelta(days=1)
|
||||
timestamp = timestamp_pb2.Timestamp()
|
||||
timestamp.FromDatetime(dt=combined)
|
||||
return timestamp
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -63,7 +98,8 @@ async def async_setup_entry(
|
||||
destination = config_entry.data[CONF_DESTINATION]
|
||||
name = config_entry.data.get(CONF_NAME, DEFAULT_NAME)
|
||||
|
||||
client = Client(api_key, timeout=10)
|
||||
client_options = ClientOptions(api_key=api_key)
|
||||
client = RoutesAsyncClient(client_options=client_options)
|
||||
|
||||
sensor = GoogleTravelTimeSensor(
|
||||
config_entry, name, api_key, origin, destination, client
|
||||
@@ -80,7 +116,15 @@ class GoogleTravelTimeSensor(SensorEntity):
|
||||
_attr_device_class = SensorDeviceClass.DURATION
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, config_entry, name, api_key, origin, destination, client):
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ConfigEntry,
|
||||
name: str,
|
||||
api_key: str,
|
||||
origin: str,
|
||||
destination: str,
|
||||
client: RoutesAsyncClient,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._attr_name = name
|
||||
self._attr_unique_id = config_entry.entry_id
|
||||
@@ -91,13 +135,12 @@ class GoogleTravelTimeSensor(SensorEntity):
|
||||
)
|
||||
|
||||
self._config_entry = config_entry
|
||||
self._matrix = None
|
||||
self._api_key = api_key
|
||||
self._route: Route | None = None
|
||||
self._client = client
|
||||
self._origin = origin
|
||||
self._destination = destination
|
||||
self._resolved_origin = None
|
||||
self._resolved_destination = None
|
||||
self._resolved_origin: str | None = None
|
||||
self._resolved_destination: str | None = None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle when entity is added."""
|
||||
@@ -109,77 +152,127 @@ class GoogleTravelTimeSensor(SensorEntity):
|
||||
await self.first_update()
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the state of the sensor."""
|
||||
if self._matrix is None:
|
||||
if self._route is None:
|
||||
return None
|
||||
|
||||
_data = self._matrix["rows"][0]["elements"][0]
|
||||
if "duration_in_traffic" in _data:
|
||||
return round(_data["duration_in_traffic"]["value"] / 60)
|
||||
if "duration" in _data:
|
||||
return round(_data["duration"]["value"] / 60)
|
||||
return None
|
||||
return round(self._route.duration.seconds / 60)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||
"""Return the state attributes."""
|
||||
if self._matrix is None:
|
||||
if self._route is None:
|
||||
return None
|
||||
|
||||
res = self._matrix.copy()
|
||||
options = self._config_entry.options.copy()
|
||||
res.update(options)
|
||||
del res["rows"]
|
||||
_data = self._matrix["rows"][0]["elements"][0]
|
||||
if "duration_in_traffic" in _data:
|
||||
res["duration_in_traffic"] = _data["duration_in_traffic"]["text"]
|
||||
if "duration" in _data:
|
||||
res["duration"] = _data["duration"]["text"]
|
||||
if "distance" in _data:
|
||||
res["distance"] = _data["distance"]["text"]
|
||||
res["origin"] = self._resolved_origin
|
||||
res["destination"] = self._resolved_destination
|
||||
return res
|
||||
result = self._config_entry.options.copy()
|
||||
result["duration_in_traffic"] = self._route.localized_values.duration.text
|
||||
result["duration"] = self._route.localized_values.static_duration.text
|
||||
result["distance"] = self._route.localized_values.distance.text
|
||||
|
||||
async def first_update(self, _=None):
|
||||
result["origin"] = self._resolved_origin
|
||||
result["destination"] = self._resolved_destination
|
||||
return result
|
||||
|
||||
async def first_update(self, _=None) -> None:
|
||||
"""Run the first update and write the state."""
|
||||
await self.hass.async_add_executor_job(self.update)
|
||||
await self.async_update()
|
||||
self.async_write_ha_state()
|
||||
|
||||
def update(self) -> None:
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data from Google."""
|
||||
options_copy = self._config_entry.options.copy()
|
||||
dtime = options_copy.get(CONF_DEPARTURE_TIME)
|
||||
atime = options_copy.get(CONF_ARRIVAL_TIME)
|
||||
if dtime is not None and ":" in dtime:
|
||||
options_copy[CONF_DEPARTURE_TIME] = convert_time_to_utc(dtime)
|
||||
elif dtime is not None:
|
||||
options_copy[CONF_DEPARTURE_TIME] = dtime
|
||||
elif atime is None:
|
||||
options_copy[CONF_DEPARTURE_TIME] = "now"
|
||||
travel_mode = TRAVEL_MODES_TO_GOOGLE_SDK_ENUM[
|
||||
self._config_entry.options[CONF_MODE]
|
||||
]
|
||||
|
||||
if atime is not None and ":" in atime:
|
||||
options_copy[CONF_ARRIVAL_TIME] = convert_time_to_utc(atime)
|
||||
elif atime is not None:
|
||||
options_copy[CONF_ARRIVAL_TIME] = atime
|
||||
if (
|
||||
departure_time := self._config_entry.options.get(CONF_DEPARTURE_TIME)
|
||||
) is not None:
|
||||
departure_time = convert_time(departure_time)
|
||||
|
||||
if (
|
||||
arrival_time := self._config_entry.options.get(CONF_ARRIVAL_TIME)
|
||||
) is not None:
|
||||
arrival_time = convert_time(arrival_time)
|
||||
if travel_mode != RouteTravelMode.TRANSIT:
|
||||
arrival_time = None
|
||||
|
||||
traffic_model = None
|
||||
routing_preference = None
|
||||
route_modifiers = None
|
||||
if travel_mode == RouteTravelMode.DRIVE:
|
||||
if (
|
||||
options_traffic_model := self._config_entry.options.get(
|
||||
CONF_TRAFFIC_MODEL
|
||||
)
|
||||
) is not None:
|
||||
traffic_model = TRAFFIC_MODELS_TO_GOOGLE_SDK_ENUM[options_traffic_model]
|
||||
routing_preference = RoutingPreference.TRAFFIC_AWARE_OPTIMAL
|
||||
route_modifiers = RouteModifiers(
|
||||
avoid_tolls=self._config_entry.options.get(CONF_AVOID) == "tolls",
|
||||
avoid_ferries=self._config_entry.options.get(CONF_AVOID) == "ferries",
|
||||
avoid_highways=self._config_entry.options.get(CONF_AVOID) == "highways",
|
||||
avoid_indoor=self._config_entry.options.get(CONF_AVOID) == "indoor",
|
||||
)
|
||||
|
||||
transit_preferences = None
|
||||
if travel_mode == RouteTravelMode.TRANSIT:
|
||||
transit_routing_preference = None
|
||||
transit_travel_mode = (
|
||||
TransitPreferences.TransitTravelMode.TRANSIT_TRAVEL_MODE_UNSPECIFIED
|
||||
)
|
||||
if (
|
||||
option_transit_preferences := self._config_entry.options.get(
|
||||
CONF_TRANSIT_ROUTING_PREFERENCE
|
||||
)
|
||||
) is not None:
|
||||
transit_routing_preference = TRANSIT_PREFS_TO_GOOGLE_SDK_ENUM[
|
||||
option_transit_preferences
|
||||
]
|
||||
if (
|
||||
option_transit_mode := self._config_entry.options.get(CONF_TRANSIT_MODE)
|
||||
) is not None:
|
||||
transit_travel_mode = TRANSPORT_TYPES_TO_GOOGLE_SDK_ENUM[
|
||||
option_transit_mode
|
||||
]
|
||||
transit_preferences = TransitPreferences(
|
||||
routing_preference=transit_routing_preference,
|
||||
allowed_travel_modes=[transit_travel_mode],
|
||||
)
|
||||
|
||||
language = None
|
||||
if (
|
||||
options_language := self._config_entry.options.get(CONF_LANGUAGE)
|
||||
) is not None:
|
||||
language = options_language
|
||||
|
||||
self._resolved_origin = find_coordinates(self.hass, self._origin)
|
||||
self._resolved_destination = find_coordinates(self.hass, self._destination)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Getting update for origin: %s destination: %s",
|
||||
self._resolved_origin,
|
||||
self._resolved_destination,
|
||||
)
|
||||
if self._resolved_destination is not None and self._resolved_origin is not None:
|
||||
request = ComputeRoutesRequest(
|
||||
origin=convert_to_waypoint(self.hass, self._resolved_origin),
|
||||
destination=convert_to_waypoint(self.hass, self._resolved_destination),
|
||||
travel_mode=travel_mode,
|
||||
routing_preference=routing_preference,
|
||||
departure_time=departure_time,
|
||||
arrival_time=arrival_time,
|
||||
route_modifiers=route_modifiers,
|
||||
language_code=language,
|
||||
units=UNITS_TO_GOOGLE_SDK_ENUM[self._config_entry.options[CONF_UNITS]],
|
||||
traffic_model=traffic_model,
|
||||
transit_preferences=transit_preferences,
|
||||
)
|
||||
try:
|
||||
self._matrix = distance_matrix(
|
||||
self._client,
|
||||
self._resolved_origin,
|
||||
self._resolved_destination,
|
||||
**options_copy,
|
||||
response = await self._client.compute_routes(
|
||||
request, metadata=[("x-goog-fieldmask", FIELD_MASK)]
|
||||
)
|
||||
except (ApiError, TransportError, Timeout) as ex:
|
||||
if response is not None and len(response.routes) > 0:
|
||||
self._route = response.routes[0]
|
||||
except GoogleAPIError as ex:
|
||||
_LOGGER.error("Error getting travel time: %s", ex)
|
||||
self._matrix = None
|
||||
self._route = None
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "You can specify the origin and destination in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`.",
|
||||
"description": "You can specify the origin and destination in the form of an address, latitude/longitude coordinates or an entity ID that provides this information in its state, an entity ID with latitude and longitude attributes, or zone friendly name (case sensitive)",
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
@@ -33,16 +33,16 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "You can optionally specify either a Departure Time or Arrival Time. If specifying a departure time, you can enter `now`, a Unix timestamp, or a 24 hour time string like `08:00:00`. If specifying an arrival time, you can use a Unix timestamp or a 24 hour time string like `08:00:00`",
|
||||
"description": "You can optionally specify either a departure time or arrival time in the form of a 24 hour time string like `08:00:00`",
|
||||
"data": {
|
||||
"mode": "Travel Mode",
|
||||
"mode": "Travel mode",
|
||||
"language": "[%key:common::config_flow::data::language%]",
|
||||
"time_type": "Time Type",
|
||||
"time_type": "Time type",
|
||||
"time": "Time",
|
||||
"avoid": "Avoid",
|
||||
"traffic_model": "Traffic Model",
|
||||
"transit_mode": "Transit Mode",
|
||||
"transit_routing_preference": "Transit Routing Preference",
|
||||
"traffic_model": "Traffic model",
|
||||
"transit_mode": "Transit mode",
|
||||
"transit_routing_preference": "Transit routing preference",
|
||||
"units": "Units"
|
||||
}
|
||||
}
|
||||
@@ -68,19 +68,19 @@
|
||||
},
|
||||
"units": {
|
||||
"options": {
|
||||
"metric": "Metric System",
|
||||
"imperial": "Imperial System"
|
||||
"metric": "Metric system",
|
||||
"imperial": "Imperial system"
|
||||
}
|
||||
},
|
||||
"time_type": {
|
||||
"options": {
|
||||
"arrival_time": "Arrival Time",
|
||||
"departure_time": "Departure Time"
|
||||
"arrival_time": "Arrival time",
|
||||
"departure_time": "Departure time"
|
||||
}
|
||||
},
|
||||
"traffic_model": {
|
||||
"options": {
|
||||
"best_guess": "Best Guess",
|
||||
"best_guess": "Best guess",
|
||||
"pessimistic": "Pessimistic",
|
||||
"optimistic": "Optimistic"
|
||||
}
|
||||
@@ -96,8 +96,8 @@
|
||||
},
|
||||
"transit_routing_preference": {
|
||||
"options": {
|
||||
"less_walking": "Less Walking",
|
||||
"fewer_transfers": "Fewer Transfers"
|
||||
"less_walking": "Less walking",
|
||||
"fewer_transfers": "Fewer transfers"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
"name": "Load consumption today (solar)"
|
||||
},
|
||||
"mix_self_consumption_today": {
|
||||
"name": "Self consumption today (solar + battery)"
|
||||
"name": "Self-consumption today (solar + battery)"
|
||||
},
|
||||
"mix_load_consumption_battery_today": {
|
||||
"name": "Load consumption today (battery)"
|
||||
@@ -173,7 +173,7 @@
|
||||
"name": "Import from grid today (load)"
|
||||
},
|
||||
"mix_last_update": {
|
||||
"name": "Last Data Update"
|
||||
"name": "Last data update"
|
||||
},
|
||||
"mix_import_from_grid_today_combined": {
|
||||
"name": "Import from grid today (load + charging)"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Constants for the habitica integration."""
|
||||
|
||||
from homeassistant.const import APPLICATION_NAME, CONF_PATH, __version__
|
||||
from homeassistant.const import APPLICATION_NAME, __version__
|
||||
|
||||
CONF_API_USER = "api_user"
|
||||
|
||||
@@ -13,15 +13,6 @@ HABITICANS_URL = "https://habitica.com/static/img/home-main@3x.ffc32b12.png"
|
||||
|
||||
DOMAIN = "habitica"
|
||||
|
||||
# service constants
|
||||
SERVICE_API_CALL = "api_call"
|
||||
ATTR_PATH = CONF_PATH
|
||||
ATTR_ARGS = "args"
|
||||
|
||||
# event constants
|
||||
EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success"
|
||||
ATTR_DATA = "data"
|
||||
|
||||
MANUFACTURER = "HabitRPG, Inc."
|
||||
NAME = "Habitica"
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.todo import ATTR_RENAME
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_DATE, ATTR_NAME, CONF_NAME
|
||||
from homeassistant.const import ATTR_DATE, ATTR_NAME
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
@@ -38,28 +38,24 @@ from homeassistant.core import (
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.selector import ConfigEntrySelector
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
ATTR_ADD_CHECKLIST_ITEM,
|
||||
ATTR_ALIAS,
|
||||
ATTR_ARGS,
|
||||
ATTR_CLEAR_DATE,
|
||||
ATTR_CLEAR_REMINDER,
|
||||
ATTR_CONFIG_ENTRY,
|
||||
ATTR_COST,
|
||||
ATTR_COUNTER_DOWN,
|
||||
ATTR_COUNTER_UP,
|
||||
ATTR_DATA,
|
||||
ATTR_DIRECTION,
|
||||
ATTR_FREQUENCY,
|
||||
ATTR_INTERVAL,
|
||||
ATTR_ITEM,
|
||||
ATTR_KEYWORD,
|
||||
ATTR_NOTES,
|
||||
ATTR_PATH,
|
||||
ATTR_PRIORITY,
|
||||
ATTR_REMINDER,
|
||||
ATTR_REMOVE_CHECKLIST_ITEM,
|
||||
@@ -78,10 +74,8 @@ from .const import (
|
||||
ATTR_UNSCORE_CHECKLIST_ITEM,
|
||||
ATTR_UP_DOWN,
|
||||
DOMAIN,
|
||||
EVENT_API_CALL_SUCCESS,
|
||||
SERVICE_ABORT_QUEST,
|
||||
SERVICE_ACCEPT_QUEST,
|
||||
SERVICE_API_CALL,
|
||||
SERVICE_CANCEL_QUEST,
|
||||
SERVICE_CAST_SKILL,
|
||||
SERVICE_CREATE_DAILY,
|
||||
@@ -106,14 +100,6 @@ from .coordinator import HabiticaConfigEntry
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SERVICE_API_CALL_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_NAME): str,
|
||||
vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]),
|
||||
vol.Optional(ATTR_ARGS): dict,
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_CAST_SKILL_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
|
||||
@@ -266,46 +252,6 @@ def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry:
|
||||
def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||
"""Set up services for Habitica integration."""
|
||||
|
||||
async def handle_api_call(call: ServiceCall) -> None:
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_api_call",
|
||||
breaks_in_ha_version="2025.6.0",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_api_call",
|
||||
)
|
||||
_LOGGER.warning(
|
||||
"Deprecated action called: 'habitica.api_call' is deprecated and will be removed in Home Assistant version 2025.6.0"
|
||||
)
|
||||
|
||||
name = call.data[ATTR_NAME]
|
||||
path = call.data[ATTR_PATH]
|
||||
entries: list[HabiticaConfigEntry] = hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
api = None
|
||||
for entry in entries:
|
||||
if entry.data[CONF_NAME] == name:
|
||||
api = await entry.runtime_data.habitica.habitipy()
|
||||
break
|
||||
if api is None:
|
||||
_LOGGER.error("API_CALL: User '%s' not configured", name)
|
||||
return
|
||||
try:
|
||||
for element in path:
|
||||
api = api[element]
|
||||
except KeyError:
|
||||
_LOGGER.error(
|
||||
"API_CALL: Path %s is invalid for API on '{%s}' element", path, element
|
||||
)
|
||||
return
|
||||
kwargs = call.data.get(ATTR_ARGS, {})
|
||||
data = await api(**kwargs)
|
||||
hass.bus.async_fire(
|
||||
EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data}
|
||||
)
|
||||
|
||||
async def cast_skill(call: ServiceCall) -> ServiceResponse:
|
||||
"""Skill action."""
|
||||
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
|
||||
@@ -928,12 +874,6 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||
schema=SERVICE_CREATE_TASK_SCHEMA,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_API_CALL,
|
||||
handle_api_call,
|
||||
schema=SERVICE_API_CALL_SCHEMA,
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
|
||||
@@ -1,20 +1,4 @@
|
||||
# Describes the format for Habitica service
|
||||
api_call:
|
||||
fields:
|
||||
name:
|
||||
required: true
|
||||
example: "xxxNotAValidNickxxx"
|
||||
selector:
|
||||
text:
|
||||
path:
|
||||
required: true
|
||||
example: '["tasks", "user", "post"]'
|
||||
selector:
|
||||
object:
|
||||
args:
|
||||
example: '{"text": "Use API from Home Assistant", "type": "todo"}'
|
||||
selector:
|
||||
object:
|
||||
cast_skill:
|
||||
fields:
|
||||
config_entry: &config_entry
|
||||
|
||||
@@ -526,31 +526,9 @@
|
||||
"deprecated_entity": {
|
||||
"title": "The Habitica {name} entity is deprecated",
|
||||
"description": "The Habitica entity `{entity}` is deprecated and will be removed in a future release.\nPlease update your automations and scripts, disable `{entity}` and reload the integration/restart Home Assistant to fix this issue."
|
||||
},
|
||||
"deprecated_api_call": {
|
||||
"title": "The Habitica action habitica.api_call is deprecated",
|
||||
"description": "The Habitica action `habitica.api_call` is deprecated and will be removed in Home Assistant 2025.5.0.\n\nPlease update your automations and scripts to use other Habitica actions and entities."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"api_call": {
|
||||
"name": "API name",
|
||||
"description": "Calls Habitica API.",
|
||||
"fields": {
|
||||
"name": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"description": "Habitica's username to call for."
|
||||
},
|
||||
"path": {
|
||||
"name": "[%key:common::config_flow::data::path%]",
|
||||
"description": "Items from API URL in form of an array with method attached at the end. Consult https://habitica.com/apidoc/. Example uses https://habitica.com/apidoc/#api-Task-CreateUserTasks."
|
||||
},
|
||||
"args": {
|
||||
"name": "Args",
|
||||
"description": "Any additional JSON or URL parameter arguments. See apidoc mentioned for path. Example uses same API endpoint."
|
||||
}
|
||||
}
|
||||
},
|
||||
"cast_skill": {
|
||||
"name": "Cast a skill",
|
||||
"description": "Uses a skill or spell from your Habitica character on a specific task to affect its progress or status.",
|
||||
|
||||
@@ -385,18 +385,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
)
|
||||
|
||||
last_timezone = None
|
||||
last_country = None
|
||||
|
||||
async def push_config(_: Event | None) -> None:
|
||||
"""Push core config to Hass.io."""
|
||||
nonlocal last_timezone
|
||||
nonlocal last_country
|
||||
|
||||
new_timezone = str(hass.config.time_zone)
|
||||
new_country = str(hass.config.country)
|
||||
|
||||
if new_timezone == last_timezone:
|
||||
return
|
||||
|
||||
last_timezone = new_timezone
|
||||
await hassio.update_hass_timezone(new_timezone)
|
||||
if new_timezone != last_timezone or new_country != last_country:
|
||||
last_timezone = new_timezone
|
||||
last_country = new_country
|
||||
await hassio.update_hass_config(new_timezone, new_country)
|
||||
|
||||
hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config)
|
||||
|
||||
|
||||
@@ -248,12 +248,14 @@ class HassIO:
|
||||
return await self.send_command("/homeassistant/options", payload=options)
|
||||
|
||||
@_api_bool
|
||||
def update_hass_timezone(self, timezone: str) -> Coroutine:
|
||||
def update_hass_config(self, timezone: str, country: str | None) -> Coroutine:
|
||||
"""Update Home-Assistant timezone data on Hass.io.
|
||||
|
||||
This method returns a coroutine.
|
||||
"""
|
||||
return self.send_command("/supervisor/options", payload={"timezone": timezone})
|
||||
return self.send_command(
|
||||
"/supervisor/options", payload={"timezone": timezone, "country": country}
|
||||
)
|
||||
|
||||
@_api_bool
|
||||
def update_diagnostics(self, diagnostics: bool) -> Coroutine:
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
from aiohomeconnect.client import Client as HomeConnectClient
|
||||
import aiohttp
|
||||
import jwt
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -110,25 +111,39 @@ async def async_migrate_entry(
|
||||
"""Migrate old entry."""
|
||||
_LOGGER.debug("Migrating from version %s", entry.version)
|
||||
|
||||
if entry.version == 1 and entry.minor_version == 1:
|
||||
if entry.version == 1:
|
||||
match entry.minor_version:
|
||||
case 1:
|
||||
|
||||
@callback
|
||||
def update_unique_id(
|
||||
entity_entry: RegistryEntry,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Update unique ID of entity entry."""
|
||||
for old_id_suffix, new_id_suffix in OLD_NEW_UNIQUE_ID_SUFFIX_MAP.items():
|
||||
if entity_entry.unique_id.endswith(f"-{old_id_suffix}"):
|
||||
return {
|
||||
"new_unique_id": entity_entry.unique_id.replace(
|
||||
old_id_suffix, new_id_suffix
|
||||
)
|
||||
}
|
||||
return None
|
||||
@callback
|
||||
def update_unique_id(
|
||||
entity_entry: RegistryEntry,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Update unique ID of entity entry."""
|
||||
for (
|
||||
old_id_suffix,
|
||||
new_id_suffix,
|
||||
) in OLD_NEW_UNIQUE_ID_SUFFIX_MAP.items():
|
||||
if entity_entry.unique_id.endswith(f"-{old_id_suffix}"):
|
||||
return {
|
||||
"new_unique_id": entity_entry.unique_id.replace(
|
||||
old_id_suffix, new_id_suffix
|
||||
)
|
||||
}
|
||||
return None
|
||||
|
||||
await async_migrate_entries(hass, entry.entry_id, update_unique_id)
|
||||
await async_migrate_entries(hass, entry.entry_id, update_unique_id)
|
||||
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
case 2:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
minor_version=3,
|
||||
unique_id=jwt.decode(
|
||||
entry.data["token"]["access_token"],
|
||||
options={"verify_signature": False},
|
||||
)["sub"],
|
||||
)
|
||||
|
||||
_LOGGER.debug("Migration to version %s successful", entry.version)
|
||||
return True
|
||||
|
||||
@@ -4,6 +4,7 @@ from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import jwt
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
||||
@@ -19,7 +20,7 @@ class OAuth2FlowHandler(
|
||||
|
||||
DOMAIN = DOMAIN
|
||||
|
||||
MINOR_VERSION = 2
|
||||
MINOR_VERSION = 3
|
||||
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
@@ -45,9 +46,15 @@ class OAuth2FlowHandler(
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
||||
"""Create an oauth config entry or update existing entry for reauth."""
|
||||
await self.async_set_unique_id(
|
||||
jwt.decode(
|
||||
data["token"]["access_token"], options={"verify_signature": False}
|
||||
)["sub"]
|
||||
)
|
||||
if self.source == SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_account")
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(),
|
||||
data_updates=data,
|
||||
self._get_reauth_entry(), data_updates=data
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return await super().async_oauth_create_entry(data)
|
||||
|
||||
@@ -4,8 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiohomeconnect.client import Client as HomeConnectClient
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
@@ -14,7 +12,7 @@ from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
||||
|
||||
|
||||
async def _generate_appliance_diagnostics(
|
||||
client: HomeConnectClient, appliance: HomeConnectApplianceData
|
||||
appliance: HomeConnectApplianceData,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
**appliance.info.to_dict(),
|
||||
@@ -31,9 +29,7 @@ async def async_get_config_entry_diagnostics(
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
return {
|
||||
appliance.info.ha_id: await _generate_appliance_diagnostics(
|
||||
entry.runtime_data.client, appliance
|
||||
)
|
||||
appliance.info.ha_id: await _generate_appliance_diagnostics(appliance)
|
||||
for appliance in entry.runtime_data.data.values()
|
||||
}
|
||||
|
||||
@@ -45,6 +41,4 @@ async def async_get_device_diagnostics(
|
||||
ha_id = next(
|
||||
(identifier[1] for identifier in device.identifiers if identifier[0] == DOMAIN),
|
||||
)
|
||||
return await _generate_appliance_diagnostics(
|
||||
entry.runtime_data.client, entry.runtime_data.data[ha_id]
|
||||
)
|
||||
return await _generate_appliance_diagnostics(entry.runtime_data.data[ha_id])
|
||||
|
||||
@@ -32,7 +32,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class HomeConnectEntity(CoordinatorEntity[HomeConnectCoordinator]):
|
||||
"""Generic Home Connect entity (base class)."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -39,11 +39,11 @@ PARALLEL_UPDATES = 1
|
||||
class HomeConnectLightEntityDescription(LightEntityDescription):
|
||||
"""Light entity description."""
|
||||
|
||||
brightness_key: SettingKey | None = None
|
||||
brightness_key: SettingKey
|
||||
brightness_scale: tuple[float, float]
|
||||
color_key: SettingKey | None = None
|
||||
enable_custom_color_value_key: str | None = None
|
||||
custom_color_key: SettingKey | None = None
|
||||
brightness_scale: tuple[float, float] = (0.0, 100.0)
|
||||
|
||||
|
||||
LIGHTS: tuple[HomeConnectLightEntityDescription, ...] = (
|
||||
|
||||
@@ -4,9 +4,23 @@
|
||||
"codeowners": ["@DavidMStraub", "@Diegorro98", "@MartinHjelmare"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials", "repairs"],
|
||||
"dhcp": [
|
||||
{
|
||||
"hostname": "balay-*",
|
||||
"macaddress": "C8D778*"
|
||||
},
|
||||
{
|
||||
"hostname": "(bosch|siemens)-*",
|
||||
"macaddress": "68A40E*"
|
||||
},
|
||||
{
|
||||
"hostname": "siemens-*",
|
||||
"macaddress": "38B4D3*"
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
"requirements": ["aiohomeconnect==0.17.0"],
|
||||
"single_config_entry": true
|
||||
"zeroconf": ["_homeconnect._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.components.number import (
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -79,7 +80,7 @@ NUMBERS = (
|
||||
NumberEntityDescription(
|
||||
key=SettingKey.COOKING_HOOD_COLOR_TEMPERATURE_PERCENT,
|
||||
translation_key="color_temperature_percent",
|
||||
native_unit_of_measurement="%",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key=SettingKey.LAUNDRY_CARE_WASHER_I_DOS_1_BASE_LEVEL,
|
||||
|
||||
@@ -11,7 +11,7 @@ from aiohomeconnect.model.error import HomeConnectError
|
||||
from aiohomeconnect.model.program import Execution
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -366,16 +366,37 @@ class HomeConnectProgramSelectEntity(HomeConnectEntity, SelectEntity):
|
||||
appliance,
|
||||
desc,
|
||||
)
|
||||
self.set_options()
|
||||
|
||||
def set_options(self) -> None:
|
||||
"""Set the options for the entity."""
|
||||
self._attr_options = [
|
||||
PROGRAMS_TRANSLATION_KEYS_MAP[program.key]
|
||||
for program in appliance.programs
|
||||
for program in self.appliance.programs
|
||||
if program.key != ProgramKey.UNKNOWN
|
||||
and (
|
||||
program.constraints is None
|
||||
or program.constraints.execution in desc.allowed_executions
|
||||
or program.constraints.execution
|
||||
in self.entity_description.allowed_executions
|
||||
)
|
||||
]
|
||||
|
||||
@callback
|
||||
def refresh_options(self) -> None:
|
||||
"""Refresh the options for the entity."""
|
||||
self.set_options()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
self.coordinator.async_add_listener(
|
||||
self.refresh_options,
|
||||
(self.appliance.info.ha_id, EventKey.BSH_COMMON_APPLIANCE_CONNECTED),
|
||||
)
|
||||
)
|
||||
|
||||
def update_native_value(self) -> None:
|
||||
"""Set the program value."""
|
||||
event = self.appliance.events.get(cast(EventKey, self.bsh_key))
|
||||
|
||||
@@ -159,7 +159,6 @@ SENSORS = (
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=StatusKey.BSH_COMMON_BATTERY_LEVEL,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
translation_key="battery_level",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=StatusKey.BSH_COMMON_VIDEO_CAMERA_STATE,
|
||||
|
||||
@@ -14,13 +14,15 @@
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]"
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||
"wrong_account": "Please ensure you reconfigure against the same account."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
@@ -1549,34 +1551,39 @@
|
||||
}
|
||||
},
|
||||
"coffee_counter": {
|
||||
"name": "Coffees"
|
||||
"name": "Coffees",
|
||||
"unit_of_measurement": "coffees"
|
||||
},
|
||||
"powder_coffee_counter": {
|
||||
"name": "Powder coffees"
|
||||
"name": "Powder coffees",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::coffee_counter::unit_of_measurement%]"
|
||||
},
|
||||
"hot_water_counter": {
|
||||
"name": "Hot water"
|
||||
},
|
||||
"hot_water_cups_counter": {
|
||||
"name": "Hot water cups"
|
||||
"name": "Hot water cups",
|
||||
"unit_of_measurement": "cups"
|
||||
},
|
||||
"hot_milk_counter": {
|
||||
"name": "Hot milk cups"
|
||||
"name": "Hot milk cups",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
|
||||
},
|
||||
"frothy_milk_counter": {
|
||||
"name": "Frothy milk cups"
|
||||
"name": "Frothy milk cups",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
|
||||
},
|
||||
"milk_counter": {
|
||||
"name": "Milk cups"
|
||||
"name": "Milk cups",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
|
||||
},
|
||||
"coffee_and_milk_counter": {
|
||||
"name": "Coffee and milk cups"
|
||||
"name": "Coffee and milk cups",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
|
||||
},
|
||||
"ristretto_espresso_counter": {
|
||||
"name": "Ristretto espresso cups"
|
||||
},
|
||||
"battery_level": {
|
||||
"name": "Battery level"
|
||||
"name": "Ristretto espresso cups",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
|
||||
},
|
||||
"camera_state": {
|
||||
"name": "Camera state",
|
||||
|
||||
@@ -79,7 +79,7 @@ class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
if self.bsh_key == SettingKey.BSH_COMMON_ALARM_CLOCK:
|
||||
if self.bsh_key is SettingKey.BSH_COMMON_ALARM_CLOCK:
|
||||
automations = automations_with_entity(self.hass, self.entity_id)
|
||||
scripts = scripts_with_entity(self.hass, self.entity_id)
|
||||
items = automations + scripts
|
||||
@@ -123,7 +123,7 @@ class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity):
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Call when entity will be removed from hass."""
|
||||
if self.bsh_key == SettingKey.BSH_COMMON_ALARM_CLOCK:
|
||||
if self.bsh_key is SettingKey.BSH_COMMON_ALARM_CLOCK:
|
||||
async_delete_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
|
||||
@@ -9,10 +9,10 @@ from typing import Any
|
||||
|
||||
from homematicip.async_home import AsyncHome
|
||||
from homematicip.auth import Auth
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
from homematicip.base.enums import EventType
|
||||
from homematicip.connection.connection_context import ConnectionContextBuilder
|
||||
from homematicip.connection.rest_connection import RestConnection
|
||||
from homematicip.exceptions.connection_exceptions import HmipConnectionError
|
||||
|
||||
import homeassistant
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.0.1"]
|
||||
"requirements": ["homematicip==2.0.1.1"]
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
UnitOfEnergy,
|
||||
@@ -127,6 +128,7 @@ async def async_setup_entry(
|
||||
):
|
||||
entities.append(HomematicipTemperatureSensor(hap, device))
|
||||
entities.append(HomematicipHumiditySensor(hap, device))
|
||||
entities.append(HomematicipAbsoluteHumiditySensor(hap, device))
|
||||
elif isinstance(device, (RoomControlDeviceAnalog,)):
|
||||
entities.append(HomematicipTemperatureSensor(hap, device))
|
||||
if isinstance(
|
||||
@@ -348,6 +350,35 @@ class HomematicipTemperatureSensor(HomematicipGenericEntity, SensorEntity):
|
||||
return state_attr
|
||||
|
||||
|
||||
class HomematicipAbsoluteHumiditySensor(HomematicipGenericEntity, SensorEntity):
|
||||
"""Representation of the HomematicIP absolute humidity sensor."""
|
||||
|
||||
_attr_native_unit_of_measurement = CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the thermometer device."""
|
||||
super().__init__(hap, device, post="Absolute Humidity")
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
"""Return the state."""
|
||||
if self.functional_channel is None:
|
||||
return None
|
||||
|
||||
value = self.functional_channel.vaporAmount
|
||||
|
||||
# Handle case where value might be None
|
||||
if (
|
||||
self.functional_channel.vaporAmount is None
|
||||
or self.functional_channel.vaporAmount == ""
|
||||
):
|
||||
return None
|
||||
|
||||
# Convert from g/m³ to mg/m³
|
||||
return int(float(value) * 1000)
|
||||
|
||||
|
||||
class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity):
|
||||
"""Representation of the HomematicIP Illuminance sensor."""
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from huawei_lte_api.exceptions import (
|
||||
ResponseErrorNotSupportedException,
|
||||
)
|
||||
from requests.exceptions import Timeout
|
||||
from url_normalize import url_normalize
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
|
||||
@@ -40,7 +41,11 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
ServiceValidationError,
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
@@ -121,7 +126,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_URL): cv.url})
|
||||
SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_URL): cv.string})
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -507,26 +512,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
because the latter is not available anywhere in the UI.
|
||||
"""
|
||||
routers = hass.data[DOMAIN].routers
|
||||
if url := service.data.get(CONF_URL):
|
||||
if url := url_normalize(service.data.get(CONF_URL), default_scheme="http"):
|
||||
router = next(
|
||||
(router for router in routers.values() if router.url == url), None
|
||||
)
|
||||
elif not routers:
|
||||
_LOGGER.error("%s: no routers configured", service.service)
|
||||
return
|
||||
raise ServiceValidationError("No routers configured")
|
||||
elif len(routers) == 1:
|
||||
router = next(iter(routers.values()))
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"%s: more than one router configured, must specify one of URLs %s",
|
||||
service.service,
|
||||
sorted(router.url for router in routers.values()),
|
||||
raise ServiceValidationError(
|
||||
f"More than one router configured, must specify one of URLs {sorted(router.url for router in routers.values())}"
|
||||
)
|
||||
return
|
||||
if not router:
|
||||
_LOGGER.error("%s: router %s unavailable", service.service, url)
|
||||
return
|
||||
raise ServiceValidationError(f"Router {url} not available")
|
||||
|
||||
was_suspended = router.suspended
|
||||
if service.service == SERVICE_RESUME_INTEGRATION:
|
||||
# Login will be handled automatically on demand
|
||||
router.suspended = False
|
||||
@@ -536,7 +537,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
router.suspended = True
|
||||
_LOGGER.debug("%s: %s", service.service, "done")
|
||||
else:
|
||||
_LOGGER.error("%s: unsupported service", service.service)
|
||||
raise ServiceValidationError(f"Unsupported service {service.service}")
|
||||
if was_suspended != router.suspended:
|
||||
# Make interactive entities' availability update
|
||||
dispatcher_send(hass, UPDATE_SIGNAL, router.config_entry.unique_id)
|
||||
|
||||
for service in ADMIN_SERVICES:
|
||||
async_register_admin_service(
|
||||
|
||||
@@ -14,10 +14,11 @@ from homeassistant.components.button import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import entity_platform
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import HuaweiLteBaseEntityWithDevice
|
||||
from .entity import HuaweiLteBaseInteractiveEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -36,7 +37,7 @@ async def async_setup_entry(
|
||||
async_add_entities(buttons)
|
||||
|
||||
|
||||
class BaseButton(HuaweiLteBaseEntityWithDevice, ButtonEntity):
|
||||
class BaseButton(HuaweiLteBaseInteractiveEntity, ButtonEntity):
|
||||
"""Huawei LTE button base class."""
|
||||
|
||||
@property
|
||||
@@ -50,10 +51,7 @@ class BaseButton(HuaweiLteBaseEntityWithDevice, ButtonEntity):
|
||||
def press(self) -> None:
|
||||
"""Press button."""
|
||||
if self.router.suspended:
|
||||
_LOGGER.debug(
|
||||
"%s: ignored, integration suspended", self.entity_description.key
|
||||
)
|
||||
return
|
||||
raise ServiceValidationError("Integration is suspended")
|
||||
result = self._press()
|
||||
_LOGGER.debug("%s: %s", self.entity_description.key, result)
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ from homeassistant.core import callback
|
||||
from homeassistant.helpers.service_info.ssdp import (
|
||||
ATTR_UPNP_FRIENDLY_NAME,
|
||||
ATTR_UPNP_MANUFACTURER,
|
||||
ATTR_UPNP_MODEL_NAME,
|
||||
ATTR_UPNP_PRESENTATION_URL,
|
||||
ATTR_UPNP_SERIAL,
|
||||
ATTR_UPNP_UDN,
|
||||
@@ -276,11 +277,12 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
if TYPE_CHECKING:
|
||||
assert discovery_info.ssdp_location
|
||||
url = url_normalize(
|
||||
discovery_info.upnp.get(
|
||||
ATTR_UPNP_PRESENTATION_URL,
|
||||
f"http://{urlparse(discovery_info.ssdp_location).hostname}/",
|
||||
)
|
||||
discovery_info.upnp.get(ATTR_UPNP_PRESENTATION_URL)
|
||||
or f"http://{urlparse(discovery_info.ssdp_location).hostname}/"
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
# url_normalize only returns None if passed None, and we don't do that
|
||||
assert url is not None
|
||||
|
||||
unique_id = discovery_info.upnp.get(
|
||||
ATTR_UPNP_SERIAL, discovery_info.upnp[ATTR_UPNP_UDN]
|
||||
@@ -308,8 +310,11 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self.context.update(
|
||||
{
|
||||
"title_placeholders": {
|
||||
CONF_NAME: discovery_info.upnp.get(ATTR_UPNP_FRIENDLY_NAME)
|
||||
or "Huawei LTE"
|
||||
CONF_NAME: (
|
||||
discovery_info.upnp.get(ATTR_UPNP_MODEL_NAME)
|
||||
or discovery_info.upnp.get(ATTR_UPNP_FRIENDLY_NAME)
|
||||
or "Huawei LTE"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -66,3 +66,12 @@ class HuaweiLteBaseEntityWithDevice(HuaweiLteBaseEntity):
|
||||
connections=self.router.device_connections,
|
||||
identifiers=self.router.device_identifiers,
|
||||
)
|
||||
|
||||
|
||||
class HuaweiLteBaseInteractiveEntity(HuaweiLteBaseEntityWithDevice):
|
||||
"""Base interactive entity."""
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and not self.router.suspended
|
||||
|
||||
@@ -37,6 +37,137 @@
|
||||
"default": "mdi:antenna"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"uptime": {
|
||||
"default": "mdi:timer-outline"
|
||||
},
|
||||
"wan_ip_address": {
|
||||
"default": "mdi:ip"
|
||||
},
|
||||
"wan_ipv6_address": {
|
||||
"default": "mdi:ip"
|
||||
},
|
||||
"cell_id": {
|
||||
"default": "mdi:antenna"
|
||||
},
|
||||
"cqi0": {
|
||||
"default": "mdi:speedometer"
|
||||
},
|
||||
"cqi1": {
|
||||
"default": "mdi:speedometer"
|
||||
},
|
||||
"enodeb_id": {
|
||||
"default": "mdi:antenna"
|
||||
},
|
||||
"lac": {
|
||||
"default": "mdi:map-marker"
|
||||
},
|
||||
"nei_cellid": {
|
||||
"default": "mdi:antenna"
|
||||
},
|
||||
"nrcqi0": {
|
||||
"default": "mdi:speedometer"
|
||||
},
|
||||
"nrcqi1": {
|
||||
"default": "mdi:speedometer"
|
||||
},
|
||||
"pci": {
|
||||
"default": "mdi:antenna"
|
||||
},
|
||||
"rac": {
|
||||
"default": "mdi:map-marker"
|
||||
},
|
||||
"tac": {
|
||||
"default": "mdi:map-marker"
|
||||
},
|
||||
"sms_unread": {
|
||||
"default": "mdi:email-arrow-left"
|
||||
},
|
||||
"current_day_transfer": {
|
||||
"default": "mdi:arrow-up-down-bold"
|
||||
},
|
||||
"current_month_download": {
|
||||
"default": "mdi:download"
|
||||
},
|
||||
"current_month_upload": {
|
||||
"default": "mdi:upload"
|
||||
},
|
||||
"wifi_clients_connected": {
|
||||
"default": "mdi:wifi"
|
||||
},
|
||||
"primary_dns_server": {
|
||||
"default": "mdi:ip"
|
||||
},
|
||||
"primary_ipv6_dns_server": {
|
||||
"default": "mdi:ip"
|
||||
},
|
||||
"secondary_dns_server": {
|
||||
"default": "mdi:ip"
|
||||
},
|
||||
"secondary_ipv6_dns_server": {
|
||||
"default": "mdi:ip"
|
||||
},
|
||||
"current_connection_duration": {
|
||||
"default": "mdi:timer-outline"
|
||||
},
|
||||
"current_connection_download": {
|
||||
"default": "mdi:download"
|
||||
},
|
||||
"current_download_rate": {
|
||||
"default": "mdi:download"
|
||||
},
|
||||
"current_connection_upload": {
|
||||
"default": "mdi:upload"
|
||||
},
|
||||
"current_upload_rate": {
|
||||
"default": "mdi:upload"
|
||||
},
|
||||
"total_connected_duration": {
|
||||
"default": "mdi:timer-outline"
|
||||
},
|
||||
"total_download": {
|
||||
"default": "mdi:download"
|
||||
},
|
||||
"total_upload": {
|
||||
"default": "mdi:upload"
|
||||
},
|
||||
"sms_deleted_device": {
|
||||
"default": "mdi:email-minus"
|
||||
},
|
||||
"sms_drafts_device": {
|
||||
"default": "mdi:email-arrow-right-outline"
|
||||
},
|
||||
"sms_inbox_device": {
|
||||
"default": "mdi:email"
|
||||
},
|
||||
"sms_capacity_device": {
|
||||
"default": "mdi:email"
|
||||
},
|
||||
"sms_outbox_device": {
|
||||
"default": "mdi:email-arrow-right"
|
||||
},
|
||||
"sms_unread_device": {
|
||||
"default": "mdi:email-arrow-left"
|
||||
},
|
||||
"sms_drafts_sim": {
|
||||
"default": "mdi:email-arrow-right-outline"
|
||||
},
|
||||
"sms_inbox_sim": {
|
||||
"default": "mdi:email"
|
||||
},
|
||||
"sms_capacity_sim": {
|
||||
"default": "mdi:email"
|
||||
},
|
||||
"sms_outbox_sim": {
|
||||
"default": "mdi:email-arrow-right"
|
||||
},
|
||||
"sms_unread_sim": {
|
||||
"default": "mdi:email-arrow-left"
|
||||
},
|
||||
"sms_messages_sim": {
|
||||
"default": "mdi:email-arrow-left"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"mobile_data": {
|
||||
"default": "mdi:signal-off",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"requirements": [
|
||||
"huawei-lte-api==1.11.0",
|
||||
"stringcase==1.2.0",
|
||||
"url-normalize==2.2.0"
|
||||
"url-normalize==2.2.1"
|
||||
],
|
||||
"ssdp": [
|
||||
{
|
||||
|
||||
@@ -17,13 +17,14 @@ from homeassistant.components.select import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import UNDEFINED
|
||||
|
||||
from . import Router
|
||||
from .const import DOMAIN, KEY_NET_NET_MODE
|
||||
from .entity import HuaweiLteBaseEntityWithDevice
|
||||
from .entity import HuaweiLteBaseInteractiveEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -76,7 +77,7 @@ async def async_setup_entry(
|
||||
async_add_entities(selects, True)
|
||||
|
||||
|
||||
class HuaweiLteSelectEntity(HuaweiLteBaseEntityWithDevice, SelectEntity):
|
||||
class HuaweiLteSelectEntity(HuaweiLteBaseInteractiveEntity, SelectEntity):
|
||||
"""Huawei LTE select entity."""
|
||||
|
||||
entity_description: HuaweiSelectEntityDescription
|
||||
@@ -102,6 +103,8 @@ class HuaweiLteSelectEntity(HuaweiLteBaseEntityWithDevice, SelectEntity):
|
||||
|
||||
def select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
if self.router.suspended:
|
||||
raise ServiceValidationError("Integration is suspended")
|
||||
self.entity_description.setter_fn(option)
|
||||
|
||||
@property
|
||||
|
||||
@@ -138,7 +138,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
"uptime": HuaweiSensorEntityDescription(
|
||||
key="uptime",
|
||||
translation_key="uptime",
|
||||
icon="mdi:timer-outline",
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
@@ -146,14 +145,12 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
"WanIPAddress": HuaweiSensorEntityDescription(
|
||||
key="WanIPAddress",
|
||||
translation_key="wan_ip_address",
|
||||
icon="mdi:ip",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
"WanIPv6Address": HuaweiSensorEntityDescription(
|
||||
key="WanIPv6Address",
|
||||
translation_key="wan_ipv6_address",
|
||||
icon="mdi:ip",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
},
|
||||
@@ -181,19 +178,16 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
"cell_id": HuaweiSensorEntityDescription(
|
||||
key="cell_id",
|
||||
translation_key="cell_id",
|
||||
icon="mdi:antenna",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"cqi0": HuaweiSensorEntityDescription(
|
||||
key="cqi0",
|
||||
translation_key="cqi0",
|
||||
icon="mdi:speedometer",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"cqi1": HuaweiSensorEntityDescription(
|
||||
key="cqi1",
|
||||
translation_key="cqi1",
|
||||
icon="mdi:speedometer",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"dl_mcs": HuaweiSensorEntityDescription(
|
||||
@@ -230,13 +224,16 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
"enodeb_id": HuaweiSensorEntityDescription(
|
||||
key="enodeb_id",
|
||||
translation_key="enodeb_id",
|
||||
icon="mdi:antenna",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"ims": HuaweiSensorEntityDescription(
|
||||
key="ims",
|
||||
translation_key="ims",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"lac": HuaweiSensorEntityDescription(
|
||||
key="lac",
|
||||
translation_key="lac",
|
||||
icon="mdi:map-marker",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"ltedlfreq": HuaweiSensorEntityDescription(
|
||||
@@ -271,6 +268,11 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"nei_cellid": HuaweiSensorEntityDescription(
|
||||
key="nei_cellid",
|
||||
translation_key="nei_cellid",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"nrbler": HuaweiSensorEntityDescription(
|
||||
key="nrbler",
|
||||
translation_key="nrbler",
|
||||
@@ -279,13 +281,11 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
"nrcqi0": HuaweiSensorEntityDescription(
|
||||
key="nrcqi0",
|
||||
translation_key="nrcqi0",
|
||||
icon="mdi:speedometer",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"nrcqi1": HuaweiSensorEntityDescription(
|
||||
key="nrcqi1",
|
||||
translation_key="nrcqi1",
|
||||
icon="mdi:speedometer",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"nrdlbandwidth": HuaweiSensorEntityDescription(
|
||||
@@ -365,7 +365,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
"pci": HuaweiSensorEntityDescription(
|
||||
key="pci",
|
||||
translation_key="pci",
|
||||
icon="mdi:antenna",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"plmn": HuaweiSensorEntityDescription(
|
||||
@@ -376,7 +375,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
"rac": HuaweiSensorEntityDescription(
|
||||
key="rac",
|
||||
translation_key="rac",
|
||||
icon="mdi:map-marker",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"rrc_status": HuaweiSensorEntityDescription(
|
||||
@@ -423,6 +421,17 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
"rxlev": HuaweiSensorEntityDescription(
|
||||
key="rxlev",
|
||||
translation_key="rxlev",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"sc": HuaweiSensorEntityDescription(
|
||||
key="sc",
|
||||
translation_key="sc",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"sinr": HuaweiSensorEntityDescription(
|
||||
key="sinr",
|
||||
translation_key="sinr",
|
||||
@@ -436,7 +445,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
"tac": HuaweiSensorEntityDescription(
|
||||
key="tac",
|
||||
translation_key="tac",
|
||||
icon="mdi:map-marker",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"tdd": HuaweiSensorEntityDescription(
|
||||
@@ -480,6 +488,12 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"wdlfreq": HuaweiSensorEntityDescription(
|
||||
key="wdlfreq",
|
||||
translation_key="wdlfreq",
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
),
|
||||
#
|
||||
@@ -494,7 +508,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
"UnreadMessage": HuaweiSensorEntityDescription(
|
||||
key="UnreadMessage",
|
||||
translation_key="sms_unread",
|
||||
icon="mdi:email-arrow-left",
|
||||
),
|
||||
},
|
||||
),
|
||||
@@ -508,7 +521,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
translation_key="current_day_transfer",
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:arrow-up-down-bold",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
last_reset_item="CurrentDayDuration",
|
||||
last_reset_format_fn=format_last_reset_elapsed_seconds,
|
||||
@@ -518,7 +530,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
translation_key="current_month_download",
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:download",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
last_reset_item="MonthDuration",
|
||||
last_reset_format_fn=format_last_reset_elapsed_seconds,
|
||||
@@ -528,7 +539,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
translation_key="current_month_upload",
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:upload",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
last_reset_item="MonthDuration",
|
||||
last_reset_format_fn=format_last_reset_elapsed_seconds,
|
||||
@@ -552,32 +562,27 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
"CurrentWifiUser": HuaweiSensorEntityDescription(
|
||||
key="CurrentWifiUser",
|
||||
translation_key="wifi_clients_connected",
|
||||
icon="mdi:wifi",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"PrimaryDns": HuaweiSensorEntityDescription(
|
||||
key="PrimaryDns",
|
||||
translation_key="primary_dns_server",
|
||||
icon="mdi:ip",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"PrimaryIPv6Dns": HuaweiSensorEntityDescription(
|
||||
key="PrimaryIPv6Dns",
|
||||
translation_key="primary_ipv6_dns_server",
|
||||
icon="mdi:ip",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"SecondaryDns": HuaweiSensorEntityDescription(
|
||||
key="SecondaryDns",
|
||||
translation_key="secondary_dns_server",
|
||||
icon="mdi:ip",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"SecondaryIPv6Dns": HuaweiSensorEntityDescription(
|
||||
key="SecondaryIPv6Dns",
|
||||
translation_key="secondary_ipv6_dns_server",
|
||||
icon="mdi:ip",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
},
|
||||
@@ -590,14 +595,12 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
translation_key="current_connection_duration",
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
icon="mdi:timer-outline",
|
||||
),
|
||||
"CurrentDownload": HuaweiSensorEntityDescription(
|
||||
key="CurrentDownload",
|
||||
translation_key="current_connection_download",
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:download",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
"CurrentDownloadRate": HuaweiSensorEntityDescription(
|
||||
@@ -605,7 +608,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
translation_key="current_download_rate",
|
||||
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
icon="mdi:download",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"CurrentUpload": HuaweiSensorEntityDescription(
|
||||
@@ -613,7 +615,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
translation_key="current_connection_upload",
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:upload",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
"CurrentUploadRate": HuaweiSensorEntityDescription(
|
||||
@@ -621,7 +622,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
translation_key="current_upload_rate",
|
||||
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
icon="mdi:upload",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"TotalConnectTime": HuaweiSensorEntityDescription(
|
||||
@@ -629,7 +629,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
translation_key="total_connected_duration",
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
icon="mdi:timer-outline",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
"TotalDownload": HuaweiSensorEntityDescription(
|
||||
@@ -637,7 +636,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
translation_key="total_download",
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:download",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
"TotalUpload": HuaweiSensorEntityDescription(
|
||||
@@ -645,7 +643,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
translation_key="total_upload",
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:upload",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
},
|
||||
@@ -691,62 +688,50 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
"LocalDeleted": HuaweiSensorEntityDescription(
|
||||
key="LocalDeleted",
|
||||
translation_key="sms_deleted_device",
|
||||
icon="mdi:email-minus",
|
||||
),
|
||||
"LocalDraft": HuaweiSensorEntityDescription(
|
||||
key="LocalDraft",
|
||||
translation_key="sms_drafts_device",
|
||||
icon="mdi:email-arrow-right-outline",
|
||||
),
|
||||
"LocalInbox": HuaweiSensorEntityDescription(
|
||||
key="LocalInbox",
|
||||
translation_key="sms_inbox_device",
|
||||
icon="mdi:email",
|
||||
),
|
||||
"LocalMax": HuaweiSensorEntityDescription(
|
||||
key="LocalMax",
|
||||
translation_key="sms_capacity_device",
|
||||
icon="mdi:email",
|
||||
),
|
||||
"LocalOutbox": HuaweiSensorEntityDescription(
|
||||
key="LocalOutbox",
|
||||
translation_key="sms_outbox_device",
|
||||
icon="mdi:email-arrow-right",
|
||||
),
|
||||
"LocalUnread": HuaweiSensorEntityDescription(
|
||||
key="LocalUnread",
|
||||
translation_key="sms_unread_device",
|
||||
icon="mdi:email-arrow-left",
|
||||
),
|
||||
"SimDraft": HuaweiSensorEntityDescription(
|
||||
key="SimDraft",
|
||||
translation_key="sms_drafts_sim",
|
||||
icon="mdi:email-arrow-right-outline",
|
||||
),
|
||||
"SimInbox": HuaweiSensorEntityDescription(
|
||||
key="SimInbox",
|
||||
translation_key="sms_inbox_sim",
|
||||
icon="mdi:email",
|
||||
),
|
||||
"SimMax": HuaweiSensorEntityDescription(
|
||||
key="SimMax",
|
||||
translation_key="sms_capacity_sim",
|
||||
icon="mdi:email",
|
||||
),
|
||||
"SimOutbox": HuaweiSensorEntityDescription(
|
||||
key="SimOutbox",
|
||||
translation_key="sms_outbox_sim",
|
||||
icon="mdi:email-arrow-right",
|
||||
),
|
||||
"SimUnread": HuaweiSensorEntityDescription(
|
||||
key="SimUnread",
|
||||
translation_key="sms_unread_sim",
|
||||
icon="mdi:email-arrow-left",
|
||||
),
|
||||
"SimUsed": HuaweiSensorEntityDescription(
|
||||
key="SimUsed",
|
||||
translation_key="sms_messages_sim",
|
||||
icon="mdi:email-arrow-left",
|
||||
),
|
||||
},
|
||||
),
|
||||
@@ -842,7 +827,7 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity):
|
||||
"""Return icon for sensor."""
|
||||
if self.entity_description.icon_fn:
|
||||
return self.entity_description.icon_fn(self.state)
|
||||
return self.entity_description.icon
|
||||
return super().icon
|
||||
|
||||
@property
|
||||
def device_class(self) -> SensorDeviceClass | None:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user