mirror of
https://github.com/home-assistant/core.git
synced 2026-04-28 18:12:37 +02:00
Compare commits
1481 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1e65fb535 | |||
| fe964bc93f | |||
| 48fdc5e1b7 | |||
| 1f1fe1b7ce | |||
| 48ee57c234 | |||
| f8ea687aa4 | |||
| 4b77b00a95 | |||
| 7119c5da3a | |||
| b63ea35959 | |||
| bb345dfd09 | |||
| c05c2b7f70 | |||
| 3d07ec8696 | |||
| 3b396814ae | |||
| b2047c1aca | |||
| 2b0cff2c93 | |||
| fa7af34678 | |||
| 7563ea6217 | |||
| 08726af215 | |||
| 4fa1d6b0a1 | |||
| 3c86f1eee8 | |||
| 3a63f9fbb1 | |||
| 7b5408d20c | |||
| 058e8ba455 | |||
| bba3c0e6bb | |||
| a266976c33 | |||
| f29c051c73 | |||
| 8842b4840e | |||
| 586d2ceff6 | |||
| 69a2284a00 | |||
| 19761a25da | |||
| e4328fe34d | |||
| e91b49e7cd | |||
| 7d145cd3b8 | |||
| 962d5386c7 | |||
| 3ba985f771 | |||
| ef6718c242 | |||
| 02bcae00cf | |||
| d6cd1dffa4 | |||
| fc32f0dbd3 | |||
| cda1974e40 | |||
| 5425e82fb4 | |||
| 84f36b0d4d | |||
| 0807525e1b | |||
| 73a86b8606 | |||
| b8652e70e5 | |||
| a3f3b0bed4 | |||
| daaa68ce22 | |||
| 9ada10e0cf | |||
| 35287c381b | |||
| 2ff84b633c | |||
| c09d91765f | |||
| ac6ddf32c8 | |||
| f15d9e5956 | |||
| f95601a2e7 | |||
| 0aef0cc121 | |||
| d1bfd94d33 | |||
| 8a9c0f4fde | |||
| 3596771af1 | |||
| 7b9b457f15 | |||
| cb8597d62f | |||
| c82cfaf633 | |||
| 80802c9997 | |||
| 971579f021 | |||
| af6b8d4f66 | |||
| e9a61963f2 | |||
| b350712f9e | |||
| 51785f10c1 | |||
| 24e0627b41 | |||
| 6c453c8b49 | |||
| 904a2d1b4d | |||
| f3b64dcbe0 | |||
| 0edc2cbbab | |||
| 751f06eb58 | |||
| 9bfac71bd7 | |||
| 9499476940 | |||
| eda1eb2e35 | |||
| 075e179972 | |||
| 99e8066607 | |||
| 7ce32f0668 | |||
| dc5547d7b6 | |||
| de98bc7dcf | |||
| a71d48085a | |||
| 9e20a13936 | |||
| e164e65217 | |||
| 07998de35e | |||
| 5253dc11dc | |||
| 3f9022cd53 | |||
| 073f498c75 | |||
| c5b24e9470 | |||
| c12b7bfd18 | |||
| 1c2f583587 | |||
| 58a376e68b | |||
| 78b251e7cb | |||
| a2c65b9126 | |||
| 5e443681c3 | |||
| 13756863f1 | |||
| fd54e45aeb | |||
| 52af74c3b6 | |||
| dc111a475e | |||
| 14cb42349a | |||
| c42b50418e | |||
| 501b4e6efb | |||
| ca2099b165 | |||
| 69b55c295d | |||
| 13709b1c90 | |||
| 2c013777db | |||
| 91099ea489 | |||
| 70cea66e5b | |||
| e78bb97e84 | |||
| 732b170190 | |||
| 0a05993a4e | |||
| 42c3610685 | |||
| 4ad73da7ec | |||
| 0d14bdab24 | |||
| 157362f225 | |||
| 1aa380fdfa | |||
| 9348948afa | |||
| 14b9915914 | |||
| 607462028b | |||
| 8c07348a3d | |||
| cda52af178 | |||
| d1ccda18f7 | |||
| 9fb0b69f0a | |||
| f0848edea9 | |||
| 5be12a213d | |||
| 20b284d0e9 | |||
| 49c3376c95 | |||
| 174b5f5593 | |||
| b38e41a34a | |||
| b6350478a5 | |||
| b75af6d84a | |||
| 194485d863 | |||
| d6458bc574 | |||
| 434f1dca2c | |||
| c6ad6da6ae | |||
| be3d65538d | |||
| 297e9e265a | |||
| 119dfbddea | |||
| 970925141e | |||
| 51131beaec | |||
| c509226d17 | |||
| 067a9a0c25 | |||
| d10197d535 | |||
| 8978d197ca | |||
| afc73fdcfd | |||
| 31a24446a8 | |||
| e80caaa7cd | |||
| 2b3a504a05 | |||
| a93229bd32 | |||
| 99306a75d3 | |||
| 3a761116e4 | |||
| a6ec59d6a5 | |||
| ca51123115 | |||
| cfc58bd415 | |||
| a18f3cba32 | |||
| 6218741602 | |||
| 2285db5bb1 | |||
| 738b85c17d | |||
| b7bb185d50 | |||
| f4544cf952 | |||
| beab473dcc | |||
| 96891228c9 | |||
| a4a36b5cbd | |||
| 4a0a400e22 | |||
| fbe4195ae0 | |||
| 116fa57903 | |||
| 2399da93db | |||
| 3850bb0e57 | |||
| f45c84b2a8 | |||
| a2e60f84da | |||
| 3757289c73 | |||
| 09067a18b7 | |||
| 6eb834946b | |||
| 0e1663f259 | |||
| 0ba3a94a3b | |||
| 3562a3800f | |||
| de0efa1639 | |||
| 818cf41c22 | |||
| 25bfb16936 | |||
| 75782e6f17 | |||
| 3e5c291338 | |||
| 30163fa2e7 | |||
| 16231d8d36 | |||
| 0c0d6595d6 | |||
| a443060faa | |||
| 9807722077 | |||
| 12b485b17e | |||
| 45def46a45 | |||
| 685b921fe7 | |||
| b813aa213f | |||
| 79ec3ff484 | |||
| 63ba49ce4c | |||
| 85c7bf1dff | |||
| 894e9bab0a | |||
| b39c83efd2 | |||
| e855b92b82 | |||
| 30ee28a0d3 | |||
| 78f6b934bb | |||
| fbef3b27bd | |||
| 646f56d015 | |||
| f82d21886a | |||
| f5054d41e1 | |||
| 53f64bff49 | |||
| 65cb9b8528 | |||
| ecd16d759a | |||
| 8498e2a715 | |||
| 4fa4ba5ad0 | |||
| a953b697ce | |||
| c543743245 | |||
| 5b76fab646 | |||
| 6153705b61 | |||
| 8632420b8f | |||
| 4f89715453 | |||
| 8ca8c2191f | |||
| cb43950ccf | |||
| ddfef18183 | |||
| ac65ba7d20 | |||
| d76272d74a | |||
| 8e5daeb7dd | |||
| 5d7abae490 | |||
| f875c77af0 | |||
| c00a68383c | |||
| 5544157d5e | |||
| 70aa58913d | |||
| cc363e4ebd | |||
| 8d28b399b0 | |||
| fe76fe5408 | |||
| a7de418213 | |||
| e359a8952b | |||
| 0a9d4ef138 | |||
| 5620cfbfd8 | |||
| fb65cf48c9 | |||
| 7fd7b2c203 | |||
| 69e691f042 | |||
| f690e6de6a | |||
| ee3c2e6f80 | |||
| 5ffe301384 | |||
| e5ad6092d1 | |||
| bd79958d10 | |||
| fe485f853f | |||
| 3c67c6087a | |||
| cb7f9b5f49 | |||
| 2547563e8c | |||
| 213b370693 | |||
| 2c9ecb394d | |||
| 51a5f5793f | |||
| 33f11f2263 | |||
| 45069b623c | |||
| 5defb4dbff | |||
| bc7c3f0617 | |||
| 704c0d1eb0 | |||
| 6c864a1725 | |||
| 299c6556bb | |||
| f0fc98cb66 | |||
| cd63d14e6f | |||
| 30dfd23da8 | |||
| d39ef523b8 | |||
| b6c2fbb8c0 | |||
| 758d5469aa | |||
| ea99f88d10 | |||
| 0a8f76864c | |||
| ad522d723c | |||
| 0f41a311c8 | |||
| 412a9a050e | |||
| d5efc3abd5 | |||
| a205623d52 | |||
| 8208eecf8c | |||
| f84398eb9c | |||
| aca5adb673 | |||
| f361d01b8b | |||
| d2cef2d26e | |||
| 90524e53ec | |||
| 668d220400 | |||
| 9e28db0535 | |||
| c5807463fd | |||
| f72a9e52f5 | |||
| 619582bd03 | |||
| bcc02d7adc | |||
| a9083d5362 | |||
| dd89fa0f5b | |||
| 88d0bd5a1d | |||
| a045c2907f | |||
| bcca7655f8 | |||
| 269ef5f824 | |||
| c80a9aab71 | |||
| 33180a658a | |||
| c5955ada1a | |||
| fd7d936a0d | |||
| 84cd137bae | |||
| 3a77a638d5 | |||
| 599f4f01d0 | |||
| bd298e92d0 | |||
| fabbfd93df | |||
| 1ecbc44368 | |||
| f30217aa41 | |||
| 4d565e6089 | |||
| faaa87e36f | |||
| cd142833e7 | |||
| 434e1e5a69 | |||
| a0ef23097f | |||
| 4d7bd49d2c | |||
| a73157e739 | |||
| 6260bd9abc | |||
| ec7aaeb8e2 | |||
| 81e92e2567 | |||
| 92fed08095 | |||
| 6c1ad5aba4 | |||
| 6b1a5219a3 | |||
| b3efa472b5 | |||
| 2cc8934bbd | |||
| a22083de10 | |||
| 2c8b8007c1 | |||
| c815090ece | |||
| 94acb8102f | |||
| 8c73dcad91 | |||
| c8f7d9dd42 | |||
| b522db1daf | |||
| 338836cba2 | |||
| f5e7605502 | |||
| 22ddb18ce2 | |||
| b541dc0a97 | |||
| 15d0a01833 | |||
| 71be2073eb | |||
| e6886fc562 | |||
| 7f0f038bcd | |||
| 686ab66a52 | |||
| 7a4f953fa6 | |||
| cd0834bfbe | |||
| c598aa6964 | |||
| 5ef28932e5 | |||
| f2eac87673 | |||
| aeb920e8ef | |||
| 8540a27f0d | |||
| fe2d8a31b8 | |||
| f4efc929d6 | |||
| 15d7febffd | |||
| 0a8f5449f2 | |||
| d2179d9243 | |||
| bf1327e355 | |||
| 9afa827eab | |||
| 3ae6f8e7a0 | |||
| 56962ff907 | |||
| 719b9bdc3c | |||
| bb1dc51a6b | |||
| abbbb7df13 | |||
| 5a308d11e4 | |||
| 6bf487c3f3 | |||
| 3162b637ea | |||
| 8cc1dd8091 | |||
| 83ff038188 | |||
| 13a8d7f7a8 | |||
| a721d32889 | |||
| bce65d4f35 | |||
| daa0ddffb9 | |||
| ee7dd329f0 | |||
| 00cd07736e | |||
| 78871e1766 | |||
| bb6f739861 | |||
| 9948431012 | |||
| 4f9241be79 | |||
| 5215e674b1 | |||
| 31b12701dc | |||
| d5ff890a18 | |||
| 32221a1ec4 | |||
| a6dd56eed0 | |||
| 682eba9773 | |||
| c055972887 | |||
| 78e2514b46 | |||
| 0af6a86507 | |||
| 2367d7c168 | |||
| 8d91fd0655 | |||
| 171b8dfa89 | |||
| f299b009fa | |||
| 91e9eb0ab3 | |||
| a2b91a9ac0 | |||
| a3add179a0 | |||
| 6075becbab | |||
| 193f519366 | |||
| b6508c2ca4 | |||
| 3dc478a357 | |||
| bd407872b0 | |||
| 8b696044c3 | |||
| 1a772b6df2 | |||
| a880ad2904 | |||
| ea73f2d0f1 | |||
| 11351500ea | |||
| 86901bfd80 | |||
| d2ef60125f | |||
| 471b49f12b | |||
| 33e9e663da | |||
| 31ff44f1a6 | |||
| 9274bd7867 | |||
| e36f9eb639 | |||
| 5149932ec8 | |||
| bdd3fc7059 | |||
| c795cbc5a3 | |||
| 20dd604292 | |||
| c35a6dc044 | |||
| cbe767c9c5 | |||
| eea3b78665 | |||
| a78a553bab | |||
| 7c7af7f0df | |||
| d52ad38dca | |||
| 477384ce9b | |||
| d1be6e1c68 | |||
| 151eae4d5a | |||
| 035e0042fa | |||
| 2568db5fdf | |||
| 28b1ded702 | |||
| 236cd795b9 | |||
| 65e90b9b9f | |||
| 96c3f3f054 | |||
| bd8e90bb00 | |||
| d488bdad8a | |||
| dec6f955f3 | |||
| bdb74ca37a | |||
| 14c0a82284 | |||
| b42bd4909b | |||
| 001a1aada6 | |||
| cd28c924ac | |||
| a19c1a7ba1 | |||
| e0d3298e77 | |||
| 2296c92a3e | |||
| 66311508ad | |||
| d628463471 | |||
| a5f9c400cc | |||
| 36051d015a | |||
| 65ae221ba7 | |||
| 0fd9360249 | |||
| 55f56c6632 | |||
| 0336ffca77 | |||
| f33bd2de22 | |||
| 0599550e04 | |||
| c384d41625 | |||
| 57b0456760 | |||
| 85c9b00035 | |||
| d9df5f1fab | |||
| f3cea5160b | |||
| ac7b5a2957 | |||
| 031830f004 | |||
| 39a655e100 | |||
| 714411c072 | |||
| 94eb1031cc | |||
| fa98eb52ad | |||
| 7b1fbbd278 | |||
| b518729367 | |||
| d04c5ccc44 | |||
| d8ba32bc8e | |||
| 7ae3c2012d | |||
| 05b78a22cf | |||
| 0a5589c800 | |||
| 9fb5bceeef | |||
| f4cce71d1f | |||
| 2209c9e0f7 | |||
| 979045bed3 | |||
| d3a8a7e9be | |||
| ca63f299ff | |||
| 1e9c8ec32c | |||
| f38f3626fb | |||
| 4a3cc511a7 | |||
| b4e012fcdf | |||
| 9da9eaf338 | |||
| 422d69f2b3 | |||
| 583524e841 | |||
| 740e21a23b | |||
| 9693ca39d1 | |||
| 52a0ed6c1c | |||
| 1702a594aa | |||
| e6b7ce97f3 | |||
| 0b13274271 | |||
| 580ae1e81b | |||
| 4c802fba7e | |||
| 41031b1cad | |||
| ff59604085 | |||
| f9cac69172 | |||
| 81a8dee22a | |||
| 00d5e89951 | |||
| 748f8b78f7 | |||
| 191f49a326 | |||
| 8178c8afa0 | |||
| 557d072a4d | |||
| 2d4c96864b | |||
| 745dc0e183 | |||
| 8d63c9ccbd | |||
| 713475ddb0 | |||
| 4badc291d9 | |||
| aa83f534c1 | |||
| b3d51a061a | |||
| 7e707d757a | |||
| 8c71965557 | |||
| 4e42478ece | |||
| 03c672a4f3 | |||
| 66b5a3755c | |||
| 6c3917e927 | |||
| e895c1b2fd | |||
| dae971cd98 | |||
| 807df50eab | |||
| aa05ff03b3 | |||
| c645bbb3f8 | |||
| 319f9fda92 | |||
| f9525ebda7 | |||
| 622b92682e | |||
| a81146a227 | |||
| 579dd6785d | |||
| 84992b875a | |||
| 530dcadf19 | |||
| 4aa67ddf22 | |||
| 8e95b19c4c | |||
| 5558b33600 | |||
| 0130ac6770 | |||
| 26d22e4d62 | |||
| 532bc02d66 | |||
| 893eac0e84 | |||
| 18a6478d9a | |||
| 3d1a8fb08c | |||
| 3657a8eb07 | |||
| 83e8d1878b | |||
| 6f635adb6b | |||
| b3f4805afe | |||
| b70651a811 | |||
| dc1e330e4a | |||
| a45da11ec1 | |||
| 31c7553e68 | |||
| 44e704a6e0 | |||
| 2824919a20 | |||
| ebe0e3ace7 | |||
| e151c9c78c | |||
| 7287c847f4 | |||
| 152e17aee7 | |||
| c53adcb73b | |||
| dab4a72128 | |||
| c94e10efa7 | |||
| ca5ea9ea35 | |||
| 63a09d8e28 | |||
| b5a3c2c014 | |||
| ef887c8edc | |||
| d0eb90274d | |||
| cac375dafb | |||
| 2c20b62229 | |||
| b5c84b6b7a | |||
| e5f9668ded | |||
| e214ce690a | |||
| a2c64f65e1 | |||
| 8bad30234a | |||
| c4545b42d8 | |||
| b0a60d1c42 | |||
| e1e14bee10 | |||
| 3529aff4b1 | |||
| 16e314ccf1 | |||
| d634fbcad7 | |||
| b84ca80d55 | |||
| 41c2c621f0 | |||
| b230e62868 | |||
| 12528ec128 | |||
| 7f4a7670a2 | |||
| 9bdc1b777e | |||
| 995e982d7f | |||
| b92698e3d5 | |||
| 225052b932 | |||
| 34ae51677f | |||
| 3616a52b37 | |||
| 0128372258 | |||
| 21863cd9d7 | |||
| d67caec5c1 | |||
| 8286014ae1 | |||
| 1ff8d2279a | |||
| 5dcbc1d5d9 | |||
| 3068653cc7 | |||
| 61b1a45889 | |||
| 573d4eba02 | |||
| 09895aa601 | |||
| aa6a4c7eab | |||
| 662c44b125 | |||
| 5a80087cf4 | |||
| c28dc32168 | |||
| eef3472c43 | |||
| f9bd9f4982 | |||
| e4620a208d | |||
| c6c5661b4b | |||
| d0154e5019 | |||
| 16fb7ed21e | |||
| d0a751abe4 | |||
| c1bd83c9c0 | |||
| a04b168a19 | |||
| b3c27e9f93 | |||
| 92e237ade2 | |||
| cbc573a6b1 | |||
| 0c059cfc27 | |||
| 143ce9d7b3 | |||
| a6aa837d40 | |||
| c58b4a0066 | |||
| 5155242ba7 | |||
| 085680f6bf | |||
| 98ecaaa6d2 | |||
| 5ad199fe16 | |||
| 413cb98424 | |||
| b38c5bcaf2 | |||
| fa85dfb3b5 | |||
| f0c6a035db | |||
| 3f0c200e56 | |||
| a2259ede28 | |||
| 24c2b6fe81 | |||
| efc7350e6f | |||
| 5f525fc2a1 | |||
| f619a3e7af | |||
| 4e43492342 | |||
| 39e70071d3 | |||
| 6da0936a66 | |||
| 5257702530 | |||
| 93da5be052 | |||
| e9576452b2 | |||
| c8c6815efd | |||
| 60ef69c21d | |||
| d5b7792208 | |||
| fdfc2f4845 | |||
| 184d834a91 | |||
| 0c98bf2676 | |||
| 229e1ee26b | |||
| fdd2db6f23 | |||
| 2886863000 | |||
| bf4170938c | |||
| 6b84815c57 | |||
| 01b873f3bc | |||
| 66b1728c13 | |||
| d11668b868 | |||
| ed3f70bc3f | |||
| 008eb39c3b | |||
| a085d91a0d | |||
| 6395a0abd0 | |||
| 0de2e689f1 | |||
| 21d06fdace | |||
| c8cf13ba19 | |||
| d9a29bd486 | |||
| bd0145cb8d | |||
| d002b48335 | |||
| c66daf13d3 | |||
| 1cae0e3cd3 | |||
| de93d1d52a | |||
| c67438c515 | |||
| fa57f72f37 | |||
| 29309d1315 | |||
| 130e0db742 | |||
| 450d46f652 | |||
| 625603839c | |||
| fb66d766a8 | |||
| e5f13b4126 | |||
| 4a22f2c93e | |||
| a5c48b190a | |||
| 5e1a0e2152 | |||
| 9a5516bb1d | |||
| b9172cf4a8 | |||
| 8e4dc29226 | |||
| b152f2f9a6 | |||
| abca80dc13 | |||
| 6869369ab2 | |||
| c2dde06713 | |||
| e455c05721 | |||
| 085df1de19 | |||
| 91a1237965 | |||
| 680a6bc856 | |||
| 152912c258 | |||
| 40e8a1b11a | |||
| 69dc354669 | |||
| bbe1bf14ae | |||
| 5470d8f8a7 | |||
| 99fe4b10d0 | |||
| 886b6b08ac | |||
| 6a1e7c1cca | |||
| d17df13055 | |||
| f73502c77a | |||
| 2c37a86bc9 | |||
| fa8e976de7 | |||
| 877bca28ad | |||
| a57c65f512 | |||
| 7140826dbb | |||
| 5fea8d69d7 | |||
| 98e3b9962e | |||
| afe19147f8 | |||
| 0e7c25488c | |||
| 412e85203d | |||
| 55ec4a95fd | |||
| 6ea9e9a161 | |||
| b56e6d1ff7 | |||
| b502cdd15b | |||
| b7ba85192d | |||
| 04d45c8ada | |||
| ba0804fefa | |||
| 538b817bf1 | |||
| 7efa2d3cac | |||
| 3f872fd196 | |||
| b00f6593f1 | |||
| a63516ff71 | |||
| 55b082edb6 | |||
| b0c3ede4fd | |||
| 84bd1cd336 | |||
| 25bbfcc595 | |||
| bf05925c8b | |||
| 488d9ad75c | |||
| 2dfad3d755 | |||
| 7e759bf730 | |||
| 9678049e72 | |||
| 8602ba2679 | |||
| 78c3503b7d | |||
| fbb3b81991 | |||
| 26eaf510ee | |||
| 5c83d16995 | |||
| 388b258d6c | |||
| 2c9a5c10da | |||
| 5a68bafd69 | |||
| 33fce89a2b | |||
| 1932f61da3 | |||
| 5a231b27b9 | |||
| 5617e8c7bc | |||
| 2b5b0e9d0f | |||
| 732f553b48 | |||
| 0a53b227ed | |||
| 44b73ab7bd | |||
| 538061d512 | |||
| e307ceccb5 | |||
| ea7558c0ad | |||
| c4399b5547 | |||
| d989a83d7b | |||
| d04f3530df | |||
| 647d957ffe | |||
| a3f3c87b39 | |||
| 447b17a2a4 | |||
| eb2b92687c | |||
| 6424e3658e | |||
| d1d8754853 | |||
| c4ff7fa676 | |||
| f1fe1d3956 | |||
| fd0d60b787 | |||
| 9ddefaaacd | |||
| 5c8df048b1 | |||
| d86d85ec56 | |||
| 660f12b683 | |||
| b8238c86e6 | |||
| 754828188e | |||
| 6992a3c72b | |||
| 738d4f662a | |||
| 7f33ac72ab | |||
| 0891d814fa | |||
| ddab50edcc | |||
| c8ce4eb32d | |||
| 22aca8b7af | |||
| 770864082f | |||
| 14545660e2 | |||
| 836353015b | |||
| c57ffd4d78 | |||
| cbebfdf149 | |||
| d8ed9ca66f | |||
| 5caf8a5b83 | |||
| c05210683e | |||
| aa8dd4bb66 | |||
| ee7d6157d9 | |||
| adec1d128c | |||
| 0a2fc97696 | |||
| 447d616097 | |||
| d3102e718d | |||
| 69ee49735a | |||
| 35a99dd4a4 | |||
| 51c3397be8 | |||
| 57f0fd2ed2 | |||
| fa7a216afe | |||
| 20f4426e1d | |||
| ba30563772 | |||
| b807c104a3 | |||
| 9e6abb719a | |||
| ed2083a60d | |||
| 94db0d5eab | |||
| 06eed998b9 | |||
| fb5c2f2566 | |||
| 4f7d065230 | |||
| d034df9b93 | |||
| 6c9fc7c7a1 | |||
| ba58ef23d8 | |||
| 0a0fa96ac1 | |||
| 9cc7ef75b0 | |||
| 2e0d6d2bbf | |||
| bafef2065f | |||
| fdfe87de4c | |||
| 933d123db3 | |||
| 1f9946a1b8 | |||
| 403e30b56e | |||
| e4524d9b68 | |||
| 738100c897 | |||
| 67356de21b | |||
| 80c5bd1843 | |||
| 492883de57 | |||
| 45f1247237 | |||
| 0e76d927cf | |||
| 4769a769e0 | |||
| f2d62049ec | |||
| 751b2638ce | |||
| 120d3ee85a | |||
| 2d273a86ba | |||
| 9bbd9d8bcd | |||
| 5ff2cac077 | |||
| 74b0d058ec | |||
| 29f96e3f9c | |||
| 39b44445ec | |||
| 589622c05a | |||
| 6abe576ec9 | |||
| 75978d8837 | |||
| a2da13a0b3 | |||
| ce081d7e71 | |||
| 037e123e11 | |||
| 592b7e5594 | |||
| a963eed3a7 | |||
| 2042f2e2bd | |||
| 3580fab26e | |||
| 1817522107 | |||
| 98a9ce3a64 | |||
| 163bfb0fdd | |||
| 66f04c702c | |||
| 41c497c49e | |||
| c25a664365 | |||
| 3dec70abce | |||
| 3c2f696a23 | |||
| 54745dc1f2 | |||
| e4345c72d9 | |||
| 7acb253ae2 | |||
| 812c63eeb7 | |||
| 7f13731035 | |||
| 879178e8a2 | |||
| 4d8cedb061 | |||
| e9f0d8a550 | |||
| c5a04deb28 | |||
| f2a205e8d7 | |||
| 254aa30ad8 | |||
| de4025634a | |||
| db4af890f4 | |||
| 501c8fecec | |||
| 03edee1335 | |||
| 00b0da7d26 | |||
| bf23fc5887 | |||
| 6f746c4375 | |||
| e7c3a62569 | |||
| b1578a0c8c | |||
| 56b4d2c015 | |||
| d5ee99c450 | |||
| 7d2a305996 | |||
| 6945418805 | |||
| ccecbcb389 | |||
| 8bb51c0662 | |||
| f66edf6b86 | |||
| 70e469366b | |||
| 4a9ba865be | |||
| 0167182e2e | |||
| 11411a880d | |||
| ce47abe1d3 | |||
| b58513c19a | |||
| 4e1dab6d8b | |||
| 5ae8e1c319 | |||
| 17bf6ca591 | |||
| 256d30c38d | |||
| 5d182394c2 | |||
| 011e6863d8 | |||
| b902b590b1 | |||
| 960666e15b | |||
| 1fb59c9f11 | |||
| 2c47e83342 | |||
| 332bf95e16 | |||
| e35fc8267e | |||
| f8b4ffc0d7 | |||
| 003ee5a699 | |||
| c91d805174 | |||
| c478d19ae3 | |||
| 09169b0f06 | |||
| aa1dbee315 | |||
| daf89e5673 | |||
| 85dc81c147 | |||
| 5acf24cb53 | |||
| e3c6a2184d | |||
| 0ba0829350 | |||
| 678048e681 | |||
| 743eeeae53 | |||
| 46555c6d9a | |||
| dbaca0a723 | |||
| 9bb2959029 | |||
| 0304781fa9 | |||
| e081d28aa4 | |||
| 34aa28c72f | |||
| cfa2946db8 | |||
| 1b0779347c | |||
| 93a281e7af | |||
| 6b32e27fd3 | |||
| 79928a8c7c | |||
| 9146518e13 | |||
| e9c5172f43 | |||
| cce21ad4b9 | |||
| 79829a311c | |||
| 10ec02ca3c | |||
| bdf54491e5 | |||
| 0b05d34238 | |||
| ce2c62ae28 | |||
| 4c69a1c5f7 | |||
| 6f1f56dcaa | |||
| d0b9991232 | |||
| aacf39be8a | |||
| 1cda3f47d6 | |||
| bf055da82c | |||
| 0fb118bcd9 | |||
| 954ef7d1f5 | |||
| b091299320 | |||
| 52483e18b2 | |||
| 57e8683ed7 | |||
| 67faace978 | |||
| e4be64fcb1 | |||
| e254716615 | |||
| 1d410f4cbd | |||
| 6616793e2b | |||
| 6766961327 | |||
| dd6fc11d28 | |||
| cb5b8b212c | |||
| 66b96d096e | |||
| e86160de36 | |||
| 7617007edd | |||
| 3e065b31b3 | |||
| 5f909a6f3a | |||
| 6117a20ec6 | |||
| 93bc05bb3f | |||
| e7397ccaa7 | |||
| 91a43873a2 | |||
| 469e06fb8c | |||
| bac370e775 | |||
| 1a9da26286 | |||
| f795707c53 | |||
| 9ad1356e4b | |||
| 0f70d5fd39 | |||
| f4c6724953 | |||
| 82432d9ee7 | |||
| 8db07f3ceb | |||
| 2fe9d1ef86 | |||
| cbb1f3726c | |||
| beb122bb1a | |||
| 8d6099b055 | |||
| 7ebe11c0e6 | |||
| 12b14b46c0 | |||
| cc45201f2d | |||
| a433a163a3 | |||
| 7fd86145d1 | |||
| f244af590e | |||
| 9a7dd98d89 | |||
| 6c4beba465 | |||
| 3a46beec76 | |||
| d7c2dfc4d4 | |||
| 4efbafb003 | |||
| 1b10db28f1 | |||
| 1e988fbb04 | |||
| 9ab577aad4 | |||
| ed53469eb6 | |||
| 56aa96a00c | |||
| 99c6cdbe44 | |||
| 1fd30b73e7 | |||
| 14aace0c00 | |||
| 6eed18623b | |||
| 66ca7d5782 | |||
| a7436cbdc3 | |||
| 5e57b0272d | |||
| e16b6ab026 | |||
| e21fb14b9a | |||
| 8e099a874b | |||
| a5302a6219 | |||
| f761ac5b49 | |||
| 6988e73ddc | |||
| a88374557b | |||
| f2456b2c3a | |||
| c1a525b7aa | |||
| 9d2febd24e | |||
| 54f96bcc33 | |||
| 5582d83f7b | |||
| 2832456bcd | |||
| 070c5821e4 | |||
| 07caa8ed2d | |||
| b02f447e4d | |||
| 4fbb22e861 | |||
| 45199a341f | |||
| de5f42d7a0 | |||
| 4459dce73a | |||
| a465905467 | |||
| a47faa3ced | |||
| 7276403ab9 | |||
| 018717af4f | |||
| 274c2b8092 | |||
| bfe15a55c9 | |||
| 54ad67b810 | |||
| 4d2732df6f | |||
| 2be3291d8e | |||
| 4326cb96ea | |||
| 278894d4b4 | |||
| eb17367229 | |||
| d96191723f | |||
| b6c7b2952e | |||
| 356de12bce | |||
| 57c49d0c48 | |||
| af22b5fdbb | |||
| 9c710961f0 | |||
| 2a2da83173 | |||
| 00a52245e3 | |||
| adb30e1ec1 | |||
| 34a7fcf8d3 | |||
| 95a57a2984 | |||
| 7f39cc0aeb | |||
| 6962288e85 | |||
| fab4355cc8 | |||
| e39d84e8fc | |||
| 35f597223a | |||
| 9d61c8336d | |||
| 6fd3603b7b | |||
| 49ac5c42ee | |||
| df0db5853c | |||
| 7afc5b777c | |||
| 595aeea8cc | |||
| 02abba02d1 | |||
| 4ca1ad96f1 | |||
| 9f3beba97a | |||
| 9f86006328 | |||
| 4ac651d0b4 | |||
| 9e54abbcb5 | |||
| d5915c8811 | |||
| 0c2887df9e | |||
| 3767bac850 | |||
| 9d962d3815 | |||
| 786fd40ae8 | |||
| 5ec65dbd58 | |||
| 35878bb203 | |||
| e14d88ff55 | |||
| d04efbfe48 | |||
| 3f35cd5cd2 | |||
| 86ffd58665 | |||
| 6206392b28 | |||
| b7c36c707f | |||
| 973c32b99d | |||
| 951775bea6 | |||
| 0f2dbdf4f4 | |||
| 443ff7efe1 | |||
| 0ee6b954df | |||
| 5681acf0e1 | |||
| a94458b8bc | |||
| f3c38ba2d3 | |||
| c1acd1d860 | |||
| f4748aa63d | |||
| 31f4f618cc | |||
| 30aec4d2ab | |||
| 335abd7002 | |||
| 3b3f0e9240 | |||
| 49586d1519 | |||
| c63ded3522 | |||
| 2eb65ab314 | |||
| 402a37b435 | |||
| aa66e8ef0c | |||
| f1a1e284b7 | |||
| 08594f4e0c | |||
| 8d810588f8 | |||
| 70faad15d5 | |||
| d447843687 | |||
| 83b64e29fa | |||
| 4558a10e05 | |||
| 5ad9e81082 | |||
| ba00a14772 | |||
| 49f4d07eeb | |||
| 5d271a0d30 | |||
| 474b683d3c | |||
| d37106a360 | |||
| e115c90719 | |||
| 6ad3adf0c3 | |||
| 2a8d59be4c | |||
| 6e6e35bc3b | |||
| 795b4c8414 | |||
| 16389dc18e | |||
| e7a1c8d001 | |||
| 4efb10dae1 | |||
| f163576e78 | |||
| cad8f97e97 | |||
| 4ae6099d84 | |||
| 60dc88fa15 | |||
| 2d2c6d676d | |||
| f3879335ab | |||
| 11bc00038e | |||
| 6845e8b880 | |||
| 5741016931 | |||
| 6cbc4e7f62 | |||
| 4064df0114 | |||
| 789f850691 | |||
| efca71852b | |||
| 1967e9f309 | |||
| 6ac0c163aa | |||
| bbe20fd698 | |||
| f576743340 | |||
| 3b4a1fba5f | |||
| 1677a9bfa6 | |||
| 0d9c458705 | |||
| 57026a862d | |||
| fd05be4c52 | |||
| b1f038849e | |||
| b46c9ccc65 | |||
| 80601426cf | |||
| 9519bd2428 | |||
| be0b7f06a8 | |||
| d30c6de168 | |||
| 0fa666518e | |||
| cf454a1fa3 | |||
| a36733c4dc | |||
| bf846e0756 | |||
| c037dad093 | |||
| ce11e66e1f | |||
| f38ca7b04a | |||
| 01200ef0a8 | |||
| c5e0c78cbc | |||
| 7681caa936 | |||
| 230a2ff045 | |||
| 9d828502a3 | |||
| 28088a7e1a | |||
| 9e8171fb77 | |||
| 1660d3b28a | |||
| 2ef81a54a5 | |||
| ce6154839e | |||
| a25300b8e1 | |||
| 6fa8e71b21 | |||
| c983978a10 | |||
| 68b8b6b675 | |||
| ee4d313b10 | |||
| 5e665093c9 | |||
| 9a5f509ab9 | |||
| 8d0cd5edaa | |||
| 71726272f5 | |||
| 9c6c27ab56 | |||
| db20cf8161 | |||
| 59b6270157 | |||
| a65ba01bbe | |||
| a5d0350560 | |||
| 368993556f | |||
| 23ea17eaef | |||
| 6ace93e45b | |||
| 237a0ae03f | |||
| 6067be6f49 | |||
| a35c3d5de5 | |||
| e9c3634cb6 | |||
| 2ba4544180 | |||
| 5235ce7ae4 | |||
| 56b601e577 | |||
| f01a0586cb | |||
| ca641a097b | |||
| df2f9d9ef8 | |||
| 501301f4e0 | |||
| 89231a1a29 | |||
| fe11a6d38f | |||
| 3154c3c962 | |||
| 5031323dea | |||
| 017a9e6938 | |||
| 9e974ab30e | |||
| 30c0d6792a | |||
| 9ffb9aa824 | |||
| 9ad71711da | |||
| ef83165159 | |||
| f0108c1175 | |||
| 802aa991a9 | |||
| f055c6c7fd | |||
| 2a8b045f43 | |||
| 281f439bc9 | |||
| 71b420b433 | |||
| 2f02d0f0dc | |||
| 37cb3cbd50 | |||
| beec21c4a9 | |||
| 642f603ea2 | |||
| a3d8d76678 | |||
| c25feaa62b | |||
| f552b8221f | |||
| 55dc5392f9 | |||
| 5b93aeae38 | |||
| 33610bb1a1 | |||
| 6c3cebe413 | |||
| 5346895d9b | |||
| 05c3f08c6c | |||
| 1ce025733d | |||
| 1537ea86b8 | |||
| ec137870fa | |||
| 816ee7f53e | |||
| 6e7eeec827 | |||
| d100477a22 | |||
| 98ac6dd2c1 | |||
| 6b30969f60 | |||
| e9a6b5d662 | |||
| f95f3f9982 | |||
| 3f884a8cd1 | |||
| 10f284932e | |||
| e1c4e6dc42 | |||
| 0976e7de4e | |||
| ae1012b2f0 | |||
| bb7c4faca5 | |||
| 0b1be61336 | |||
| 3ec44024a2 | |||
| 1200cc5779 | |||
| d632931f74 | |||
| 50bde6fccd | |||
| 1b7398c271 | |||
| 7e4b8e802e | |||
| 4bcea27151 | |||
| ffca43027f | |||
| 01e94ca5b2 | |||
| b8ea6b4162 | |||
| 1471cb93bc | |||
| 2f7ac2b439 | |||
| 0accb403be | |||
| f49a323faf | |||
| 21d303dbbc | |||
| c080a460a2 | |||
| 75d675f299 | |||
| a7e7d01b7a | |||
| 8a0569e279 | |||
| e8279bd20f | |||
| 852dbf8986 | |||
| 6f0eb1d07a | |||
| 6f68d91593 | |||
| ffc17b6e91 | |||
| 0d04d79844 | |||
| f57884cb95 | |||
| 3a83fe5c72 | |||
| 973feb71c1 | |||
| ecee23fc7a | |||
| 442d2282dc | |||
| 8853d3e17d | |||
| 6d1e387911 | |||
| 13fe135e7f | |||
| 618687ea05 | |||
| 8b545a6e76 | |||
| 42fa13200d | |||
| d56e944a86 | |||
| fb357390ce | |||
| 702450e209 | |||
| bbe45e0759 | |||
| 92902c7aa1 | |||
| 5d92dd7760 | |||
| 0ab62dabde | |||
| fc68828c78 | |||
| 7644036592 | |||
| f19068f7de | |||
| 13d2211755 | |||
| 87e63591d1 | |||
| fc02bbcdd0 | |||
| 388d619604 | |||
| 3777acff95 | |||
| e0fd6784cf | |||
| 305463d882 | |||
| de16edc55b | |||
| bd6438937b | |||
| 45e453791e | |||
| 152137a3a2 | |||
| e059c51b1d | |||
| 9ef66a3a90 | |||
| 494f8c32d5 | |||
| 51f90a328b | |||
| b7bdb7b32a | |||
| 76c8bae098 | |||
| 59a75e74fe | |||
| a4af1ce5f8 | |||
| 30ea0b4923 | |||
| fb889dd524 | |||
| 31055c5cde | |||
| a264e5949f | |||
| 84260ac3f7 | |||
| f50a35877d | |||
| 6bc94a318a | |||
| b0904917ca | |||
| 536cfc4c67 | |||
| 27b647fa36 | |||
| 16fb2dfa91 | |||
| 664b75e060 | |||
| 1cd302eb17 | |||
| 8da86796d2 | |||
| 33c0edc994 | |||
| 3e8833da54 | |||
| 3858d557b3 | |||
| 0923bed4b6 | |||
| 9b8432eac3 | |||
| 5232c05702 | |||
| e5f77801a7 | |||
| bc138b3485 | |||
| ae90c5fa92 | |||
| 2fce45abe1 | |||
| e4417f7b00 | |||
| b57c7f8a95 | |||
| 0618460d73 | |||
| 92dd045772 | |||
| fc723e1a42 | |||
| 5907356309 | |||
| 1c221b4714 | |||
| 05d57167d2 | |||
| 69a98dd53e | |||
| 3c7dd93c7f | |||
| 1327712be4 | |||
| 933e57ba6a | |||
| 77d54aadc6 | |||
| 5fe2ab93ff | |||
| 0e4698eb99 | |||
| 698c5eca00 | |||
| c7776057b7 | |||
| e87c677cc4 | |||
| c3858a0841 | |||
| 42bc5c3a5f | |||
| 76bc58da2c | |||
| fc8719ce35 | |||
| 60a4a97d9c | |||
| 284721e1df | |||
| bfa707d79e | |||
| 633e2e7469 | |||
| ad1c6846e7 | |||
| f75140b626 | |||
| f83757da7c | |||
| ca338c98f3 | |||
| 2f9faa53a1 | |||
| 18a8afb017 | |||
| 718607a758 | |||
| 3789156559 | |||
| 042ce6f2de | |||
| 0a5908002f | |||
| 3a5f71e10a | |||
| 04e4b05ab0 | |||
| 0136e9c7eb | |||
| d88c736016 | |||
| 780dc178a1 | |||
| b7ba945dfc | |||
| 01de7052af | |||
| 3fe6a31ee9 | |||
| 95570643ec | |||
| c2c5232899 | |||
| 593610094e | |||
| 47cb7870ea | |||
| 045b626e24 | |||
| bea5468dee | |||
| 04fc12cc26 | |||
| fec33ad42b | |||
| 07e323f1e9 | |||
| ebe2612713 | |||
| 88ca668562 | |||
| 1d46ac0b64 | |||
| 13a5e6e85f | |||
| d2665f03ff | |||
| 80412e4973 | |||
| 818d9f774e | |||
| 012e78d625 | |||
| 74abedbcd2 | |||
| e16fb6b5a5 | |||
| 8906e5dcb5 | |||
| 10067c208a | |||
| d4143205e9 | |||
| a4da363ff2 | |||
| bc9ae3dad6 | |||
| 9e5daaa784 | |||
| ff0a6757cd | |||
| e3210b0ab9 | |||
| 2edabf903a | |||
| 0e4e703b64 | |||
| 88624f5179 | |||
| 4a5fdfc0ec | |||
| c6e91afae4 | |||
| db5e7e4521 | |||
| 25489c224b | |||
| c4f64598a0 | |||
| 59e579cf5a | |||
| 831c28cf2c | |||
| be1affc6ba | |||
| 94a25b5688 | |||
| 382940d661 | |||
| b8e1c0cf2c | |||
| 0d23d8dc09 | |||
| b750de1e3e | |||
| 7d7e8e0bde | |||
| d6f355355f | |||
| 5dad64e54c | |||
| c311ff0464 | |||
| c45675a01f | |||
| 9d92141812 | |||
| 501b973a98 | |||
| fd4d8137da | |||
| 33881c1912 | |||
| 9bdb03dbe8 | |||
| d2178ba458 | |||
| 06cdf3c5d2 | |||
| 84c994ab80 | |||
| 1d5913d7a5 | |||
| 05acba37c7 | |||
| 7496406156 | |||
| 543f2b1396 | |||
| 3df2bbda80 | |||
| b661d37a86 | |||
| 2102babc6d | |||
| f3a1cab582 | |||
| 03c9ce25c8 | |||
| 8fcabcec16 | |||
| 2a33096074 | |||
| 14a9eada09 | |||
| 4a00f78e90 | |||
| abef46864e | |||
| 73b28f1ee2 | |||
| 7379d41393 | |||
| 89acb02519 | |||
| e343e90da2 | |||
| e9a576494b | |||
| 4e047b56d8 | |||
| a1e95c483d | |||
| 9cb6e02c5f | |||
| 2c75e3289a | |||
| 348012a6b8 | |||
| e0db00e089 | |||
| b2280198d9 | |||
| 9cc4a3e427 | |||
| f94a075641 | |||
| f1856e6ef6 | |||
| ed35bafa6c | |||
| 66e16d728b | |||
| a806efa7e2 | |||
| ad4b4bd221 | |||
| c9c9a149b6 | |||
| 0f9fdfe2de | |||
| a76b63912d | |||
| bc03e13d38 | |||
| 450aa9757d | |||
| 158389a4f2 | |||
| 95e89d5ef1 | |||
| e107b8e5cd | |||
| f875b43ede | |||
| 6242ef78c4 | |||
| 3c342c0768 | |||
| 5dba5fc79d | |||
| 62ffeeccb0 | |||
| 1afe00670e | |||
| 500ffe8153 | |||
| 2cebb28a1b | |||
| 80bfba0981 | |||
| 882e499375 | |||
| e89aafc8e2 | |||
| 66ae5ab543 | |||
| 75d39c0b02 | |||
| 989133cb16 | |||
| f559f8e014 | |||
| a95207f2ef | |||
| 2c28a93ea0 | |||
| 3ff97a0820 | |||
| f7a56447ae | |||
| dfd086f253 | |||
| b6a166ce48 | |||
| e93b724ce4 | |||
| d0b25ccc01 | |||
| 0a3ef64f28 | |||
| e9ce3ffff9 | |||
| 55415b1559 | |||
| 0160dbf3a6 | |||
| 7dd83b1e8f | |||
| e502f5f249 | |||
| 6e93ebc912 | |||
| 9a4fdf7f80 | |||
| 76d69a5f53 | |||
| ae40c0cf4b | |||
| 078647d128 | |||
| 8a637c4e5b | |||
| 9e9daff26d | |||
| 41aeedaa82 | |||
| a8297ae65d | |||
| b7f1171c08 | |||
| 226f606cb9 | |||
| 9472be39f2 | |||
| 67a9e42b19 | |||
| ba1837859f | |||
| 4a301eceac | |||
| d138a99e62 | |||
| a431f84dc9 | |||
| aa9534600e | |||
| 54fa49e754 | |||
| 459b6152f4 | |||
| 60c8d997ca | |||
| a598368895 | |||
| 2ff1499c48 | |||
| 348ddbe124 | |||
| 71ed43faf2 | |||
| dc69a90296 | |||
| f5db8e6ba4 | |||
| b82a26ef68 | |||
| 0eaaeedf11 | |||
| 62e26e53ac |
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
name: ban-word-list
|
|
||||||
description: Find words that are not allowed
|
|
||||||
---
|
|
||||||
|
|
||||||
# Ban Word List
|
|
||||||
|
|
||||||
If any of the words listed in the `list.md` file are found on new code, warn the user and ask them to change it.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- potato
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: github-pr-reviewer
|
||||||
|
description: Review a GitHub pull request and provide feedback comments. Use when the user says "review the current PR" or asks to review a specific PR.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Review GitHub Pull Request
|
||||||
|
|
||||||
|
## Follow these steps:
|
||||||
|
1. Use 'gh pr view' to get the PR details and description.
|
||||||
|
2. Use 'gh pr diff' to see all the changes in the PR.
|
||||||
|
3. Analyze the code changes for:
|
||||||
|
- Code quality and style consistency
|
||||||
|
- Potential bugs or issues
|
||||||
|
- Performance implications
|
||||||
|
- Security concerns
|
||||||
|
- Test coverage
|
||||||
|
- Documentation updates if needed
|
||||||
|
4. Ensure any existing review comments have been addressed.
|
||||||
|
5. Generate constructive review comments in the CONSOLE. DO NOT POST TO GITHUB YOURSELF.
|
||||||
|
|
||||||
|
## IMPORTANT:
|
||||||
|
- Just review. DO NOT make any changes
|
||||||
|
- Be constructive and specific in your comments
|
||||||
|
- Suggest improvements where appropriate
|
||||||
|
- Only provide review feedback in the CONSOLE. DO NOT ACT ON GITHUB.
|
||||||
|
- No need to run tests or linters, just review the code changes.
|
||||||
|
- No need to highlight things that are already good.
|
||||||
|
|
||||||
|
## Output format:
|
||||||
|
- List specific comments for each file/line that needs attention
|
||||||
|
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
|
||||||
|
- Example output:
|
||||||
|
```
|
||||||
|
Overall assessment: request changes.
|
||||||
|
- [CRITICAL] Memory leak in homeassistant/components/sensor/my_sensor.py:143
|
||||||
|
- [PROBLEM] Inefficient algorithm in homeassistant/helpers/data_processing.py:87
|
||||||
|
- [SUGGESTION] Improve variable naming in homeassistant/helpers/config_validation.py:45
|
||||||
|
```
|
||||||
@@ -620,12 +620,14 @@ rules:
|
|||||||
|
|
||||||
### Config Flow Testing
|
### Config Flow Testing
|
||||||
- **100% Coverage Required**: All config flow paths must be tested
|
- **100% Coverage Required**: All config flow paths must be tested
|
||||||
|
- **Patch Boundaries**: Only patch library or client methods when testing config flows. Do not patch methods defined in `config_flow.py`; exercise the flow logic end-to-end.
|
||||||
- **Test Scenarios**:
|
- **Test Scenarios**:
|
||||||
- All flow initiation methods (user, discovery, import)
|
- All flow initiation methods (user, discovery, import)
|
||||||
- Successful configuration paths
|
- Successful configuration paths
|
||||||
- Error recovery scenarios
|
- Error recovery scenarios
|
||||||
- Prevention of duplicate entries
|
- Prevention of duplicate entries
|
||||||
- Flow completion after errors
|
- Flow completion after errors
|
||||||
|
- Reauthentication/reconfigure flows
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
- **Integration-specific tests** (recommended):
|
- **Integration-specific tests** (recommended):
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Dockerfile.dev linguist-language=Dockerfile
|
|||||||
CODEOWNERS linguist-generated=true
|
CODEOWNERS linguist-generated=true
|
||||||
Dockerfile linguist-generated=true
|
Dockerfile linguist-generated=true
|
||||||
homeassistant/generated/*.py linguist-generated=true
|
homeassistant/generated/*.py linguist-generated=true
|
||||||
|
machine/* linguist-generated=true
|
||||||
mypy.ini linguist-generated=true
|
mypy.ini linguist-generated=true
|
||||||
requirements.txt linguist-generated=true
|
requirements.txt linguist-generated=true
|
||||||
requirements_all.txt linguist-generated=true
|
requirements_all.txt linguist-generated=true
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
<!-- Automatically generated by gen_copilot_instructions.py, do not edit -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Copilot code review instructions
|
||||||
|
|
||||||
|
- Start review comments with a short, one-sentence summary of the suggested fix.
|
||||||
|
- Do not add comments about code style, formatting or linting issues.
|
||||||
|
|
||||||
# GitHub Copilot & Claude Code Instructions
|
# GitHub Copilot & Claude Code Instructions
|
||||||
|
|
||||||
@@ -5,50 +13,27 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
|||||||
|
|
||||||
## Code Review Guidelines
|
## Code Review Guidelines
|
||||||
|
|
||||||
**When reviewing code, do NOT comment on:**
|
|
||||||
- **Missing imports** - We use static analysis tooling to catch that
|
|
||||||
- **Code formatting** - We have ruff as a formatting tool that will catch those if needed (unless specifically instructed otherwise in these instructions)
|
|
||||||
|
|
||||||
**Git commit practices during review:**
|
**Git commit practices during review:**
|
||||||
- **Do NOT amend, squash, or rebase commits after review has started** - Reviewers need to see what changed since their last review
|
- **Do NOT amend, squash, or rebase commits after review has started** - Reviewers need to see what changed since their last review
|
||||||
|
|
||||||
## Python Requirements
|
## Development Commands
|
||||||
|
|
||||||
- **Compatibility**: Python 3.13+
|
.vscode/tasks.json contains useful commands used for development.
|
||||||
- **Language Features**: Use the newest features when possible:
|
|
||||||
- Pattern matching
|
|
||||||
- Type hints
|
|
||||||
- f-strings (preferred over `%` or `.format()`)
|
|
||||||
- Dataclasses
|
|
||||||
- Walrus operator
|
|
||||||
|
|
||||||
### Strict Typing (Platinum)
|
## Python Syntax Notes
|
||||||
- **Comprehensive Type Hints**: Add type hints to all functions, methods, and variables
|
|
||||||
- **Custom Config Entry Types**: When using runtime_data:
|
|
||||||
```python
|
|
||||||
type MyIntegrationConfigEntry = ConfigEntry[MyClient]
|
|
||||||
```
|
|
||||||
- **Library Requirements**: Include `py.typed` file for PEP-561 compliance
|
|
||||||
|
|
||||||
## Code Quality Standards
|
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses.
|
||||||
|
|
||||||
- **Formatting**: Ruff
|
## Testing
|
||||||
- **Linting**: PyLint and Ruff
|
|
||||||
- **Type Checking**: MyPy
|
|
||||||
- **Lint/Type/Format Fixes**: Always prefer addressing the underlying issue (e.g., import the typed source, update shared stubs, align with Ruff expectations, or correct formatting at the source) before disabling a rule, adding `# type: ignore`, or skipping a formatter. Treat suppressions and `noqa` comments as a last resort once no compliant fix exists
|
|
||||||
- **Testing**: pytest with plain functions and fixtures
|
|
||||||
- **Language**: American English for all code, comments, and documentation (use sentence case, including titles)
|
|
||||||
|
|
||||||
### Writing Style Guidelines
|
When writing or modifying tests, ensure all test function parameters have type annotations.
|
||||||
- **Tone**: Friendly and informative
|
Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) over `Any`.
|
||||||
- **Perspective**: Use second-person ("you" and "your") for user-facing messages
|
|
||||||
- **Inclusivity**: Use objective, non-discriminatory language
|
|
||||||
- **Clarity**: Write for non-native English speakers
|
|
||||||
- **Formatting in Messages**:
|
|
||||||
- Use backticks for: file paths, filenames, variable names, field entries
|
|
||||||
- Use sentence case for titles and messages (capitalize only the first word and proper nouns)
|
|
||||||
- Avoid abbreviations when possible
|
|
||||||
|
|
||||||
### Skill files
|
## Good practices
|
||||||
|
|
||||||
- ban-word-list: /.claude/skills/ban-word-list/SKILL.md
|
Integrations with Platinum or Gold level in the Integration Quality Scale reflect a high standard of code quality and maintainability. When looking for examples of something, these are good places to start. The level is indicated in the manifest.json of the integration.
|
||||||
|
|
||||||
|
|
||||||
|
# Skills
|
||||||
|
|
||||||
|
- Home Assistant Integration knowledge: .claude/skills/integrations/SKILL.md
|
||||||
|
|||||||
+57
-116
@@ -10,7 +10,6 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
BUILD_TYPE: core
|
BUILD_TYPE: core
|
||||||
DEFAULT_PYTHON: "3.14.2"
|
|
||||||
PIP_TIMEOUT: 60
|
PIP_TIMEOUT: 60
|
||||||
UV_HTTP_TIMEOUT: 60
|
UV_HTTP_TIMEOUT: 60
|
||||||
UV_SYSTEM_PYTHON: "true"
|
UV_SYSTEM_PYTHON: "true"
|
||||||
@@ -36,16 +35,17 @@ jobs:
|
|||||||
channel: ${{ steps.version.outputs.channel }}
|
channel: ${{ steps.version.outputs.channel }}
|
||||||
publish: ${{ steps.version.outputs.publish }}
|
publish: ${{ steps.version.outputs.publish }}
|
||||||
architectures: ${{ env.ARCHITECTURES }}
|
architectures: ${{ env.ARCHITECTURES }}
|
||||||
|
base_image_version: ${{ env.BASE_IMAGE_VERSION }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version-file: ".python-version"
|
||||||
|
|
||||||
- name: Get information
|
- name: Get information
|
||||||
id: info
|
id: info
|
||||||
@@ -73,14 +73,14 @@ jobs:
|
|||||||
- name: Download Translations
|
- name: Download Translations
|
||||||
run: python3 -m script.translations download
|
run: python3 -m script.translations download
|
||||||
env:
|
env:
|
||||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
|
||||||
|
|
||||||
- name: Archive translations
|
- name: Archive translations
|
||||||
shell: bash
|
shell: bash
|
||||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||||
|
|
||||||
- name: Upload translations
|
- name: Upload translations
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
path: translations.tar.gz
|
path: translations.tar.gz
|
||||||
@@ -101,7 +101,7 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
include:
|
include:
|
||||||
- arch: amd64
|
- arch: amd64
|
||||||
os: ubuntu-latest
|
os: ubuntu-24.04
|
||||||
- arch: aarch64
|
- arch: aarch64
|
||||||
os: ubuntu-24.04-arm
|
os: ubuntu-24.04-arm
|
||||||
steps:
|
steps:
|
||||||
@@ -112,7 +112,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download nightly wheels of frontend
|
- name: Download nightly wheels of frontend
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: dawidd6/action-download-artifact@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14
|
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
|
||||||
with:
|
with:
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||||
repo: home-assistant/frontend
|
repo: home-assistant/frontend
|
||||||
@@ -123,7 +123,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download nightly wheels of intents
|
- name: Download nightly wheels of intents
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: dawidd6/action-download-artifact@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14
|
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
|
||||||
with:
|
with:
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||||
repo: OHF-Voice/intents-package
|
repo: OHF-Voice/intents-package
|
||||||
@@ -132,11 +132,11 @@ jobs:
|
|||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
name: package
|
name: package
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version-file: ".python-version"
|
||||||
|
|
||||||
- name: Adjust nightly version
|
- name: Adjust nightly version
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
@@ -182,7 +182,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -196,77 +196,20 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
|
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Install Cosign
|
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
|
||||||
with:
|
|
||||||
cosign-release: "v2.5.3"
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
|
||||||
|
|
||||||
- name: Build variables
|
|
||||||
id: vars
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
ARCH: ${{ matrix.arch }}
|
|
||||||
run: |
|
|
||||||
echo "base_image=ghcr.io/home-assistant/${ARCH}-homeassistant-base:${BASE_IMAGE_VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "cache_image=ghcr.io/home-assistant/${ARCH}-homeassistant:latest" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "created=$(date --rfc-3339=seconds --utc)" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Verify base image signature
|
|
||||||
env:
|
|
||||||
BASE_IMAGE: ${{ steps.vars.outputs.base_image }}
|
|
||||||
run: |
|
|
||||||
cosign verify \
|
|
||||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
|
||||||
--certificate-identity-regexp "https://github.com/home-assistant/docker/.*" \
|
|
||||||
"${BASE_IMAGE}"
|
|
||||||
|
|
||||||
- name: Verify cache image signature
|
|
||||||
id: cache
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
CACHE_IMAGE: ${{ steps.vars.outputs.cache_image }}
|
|
||||||
run: |
|
|
||||||
cosign verify \
|
|
||||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
|
||||||
--certificate-identity-regexp "https://github.com/home-assistant/core/.*" \
|
|
||||||
"${CACHE_IMAGE}"
|
|
||||||
|
|
||||||
- name: Build base image
|
- name: Build base image
|
||||||
id: build
|
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
|
||||||
with:
|
with:
|
||||||
context: .
|
arch: ${{ matrix.arch }}
|
||||||
file: ./Dockerfile
|
|
||||||
platforms: ${{ steps.vars.outputs.platform }}
|
|
||||||
push: true
|
|
||||||
cache-from: ${{ steps.cache.outcome == 'success' && steps.vars.outputs.cache_image || '' }}
|
|
||||||
build-args: |
|
build-args: |
|
||||||
BUILD_FROM=${{ steps.vars.outputs.base_image }}
|
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
|
||||||
tags: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
|
cache-gha: false
|
||||||
outputs: type=image,push=true,compression=zstd,compression-level=9,force-compression=true,oci-mediatypes=true
|
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
labels: |
|
cosign-base-identity: "https://github.com/home-assistant/docker/.*"
|
||||||
io.hass.arch=${{ matrix.arch }}
|
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
|
||||||
io.hass.version=${{ needs.init.outputs.version }}
|
image: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant
|
||||||
org.opencontainers.image.created=${{ steps.vars.outputs.created }}
|
image-tags: ${{ needs.init.outputs.version }}
|
||||||
org.opencontainers.image.version=${{ needs.init.outputs.version }}
|
push: true
|
||||||
|
version: ${{ needs.init.outputs.version }}
|
||||||
- name: Sign image
|
|
||||||
env:
|
|
||||||
ARCH: ${{ matrix.arch }}
|
|
||||||
VERSION: ${{ needs.init.outputs.version }}
|
|
||||||
DIGEST: ${{ steps.build.outputs.digest }}
|
|
||||||
run: |
|
|
||||||
cosign sign --yes "ghcr.io/home-assistant/${ARCH}-homeassistant:${VERSION}@${DIGEST}"
|
|
||||||
|
|
||||||
build_machine:
|
build_machine:
|
||||||
name: Build ${{ matrix.machine }} machine core image
|
name: Build ${{ matrix.machine }} machine core image
|
||||||
@@ -281,7 +224,6 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
machine:
|
machine:
|
||||||
- generic-x86-64
|
- generic-x86-64
|
||||||
- intel-nuc
|
|
||||||
- khadas-vim3
|
- khadas-vim3
|
||||||
- odroid-c2
|
- odroid-c2
|
||||||
- odroid-c4
|
- odroid-c4
|
||||||
@@ -305,45 +247,44 @@ jobs:
|
|||||||
- machine: qemux86-64
|
- machine: qemux86-64
|
||||||
arch: amd64
|
arch: amd64
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
# TODO: remove, intel-nuc is a legacy name for x86-64, renamed in 2021
|
|
||||||
- machine: intel-nuc
|
|
||||||
arch: amd64
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set build additional args
|
- name: Compute extra tags
|
||||||
|
id: tags
|
||||||
|
shell: bash
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.init.outputs.version }}
|
VERSION: ${{ needs.init.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
# Create general tags
|
|
||||||
if [[ "${VERSION}" =~ d ]]; then
|
if [[ "${VERSION}" =~ d ]]; then
|
||||||
echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
|
echo "extra_tags=dev" >> "$GITHUB_OUTPUT"
|
||||||
elif [[ "${VERSION}" =~ b ]]; then
|
elif [[ "${VERSION}" =~ b ]]; then
|
||||||
echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
|
echo "extra_tags=beta" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
|
echo "extra_tags=stable" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Build machine image
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
arch: ${{ matrix.arch }}
|
||||||
username: ${{ github.repository_owner }}
|
build-args: |
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
|
||||||
|
cache-gha: false
|
||||||
- name: Build base image
|
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
uses: home-assistant/builder@6cb4fd3d1338b6e22d0958a4bcb53e0965ea63b4 # 2026.02.1
|
context: machine/
|
||||||
with:
|
cosign-base-identity: "https://github.com/home-assistant/core/.*"
|
||||||
image: ${{ matrix.arch }}
|
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
|
||||||
args: |
|
file: machine/${{ matrix.machine }}
|
||||||
$BUILD_ARGS \
|
image: ghcr.io/home-assistant/${{ matrix.machine }}-homeassistant
|
||||||
--target /data/machine \
|
image-tags: |
|
||||||
--cosign \
|
${{ needs.init.outputs.version }}
|
||||||
--machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
|
${{ steps.tags.outputs.extra_tags }}
|
||||||
|
push: true
|
||||||
|
version: ${{ needs.init.outputs.version }}
|
||||||
|
|
||||||
publish_ha:
|
publish_ha:
|
||||||
name: Publish version files
|
name: Publish version files
|
||||||
@@ -401,19 +342,19 @@ jobs:
|
|||||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||||
steps:
|
steps:
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.5.3"
|
cosign-release: "v2.5.3"
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: matrix.registry == 'docker.io/homeassistant'
|
if: matrix.registry == 'docker.io/homeassistant'
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -443,7 +384,7 @@ jobs:
|
|||||||
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
|
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
|
||||||
- name: Generate Docker metadata
|
- name: Generate Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||||
with:
|
with:
|
||||||
images: ${{ matrix.registry }}/home-assistant
|
images: ${{ matrix.registry }}/home-assistant
|
||||||
sep-tags: ","
|
sep-tags: ","
|
||||||
@@ -457,7 +398,7 @@ jobs:
|
|||||||
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.7.1
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v3.7.1
|
||||||
|
|
||||||
- name: Copy architecture images to DockerHub
|
- name: Copy architecture images to DockerHub
|
||||||
if: matrix.registry == 'docker.io/homeassistant'
|
if: matrix.registry == 'docker.io/homeassistant'
|
||||||
@@ -538,13 +479,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version-file: ".python-version"
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -586,14 +527,14 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||||
with:
|
with:
|
||||||
context: . # So action will not pull the repository again
|
context: . # So action will not pull the repository again
|
||||||
file: ./script/hassfest/docker/Dockerfile
|
file: ./script/hassfest/docker/Dockerfile
|
||||||
@@ -606,7 +547,7 @@ jobs:
|
|||||||
- name: Push Docker image
|
- name: Push Docker image
|
||||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||||
id: push
|
id: push
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||||
with:
|
with:
|
||||||
context: . # So action will not pull the repository again
|
context: . # So action will not pull the repository again
|
||||||
file: ./script/hassfest/docker/Dockerfile
|
file: ./script/hassfest/docker/Dockerfile
|
||||||
@@ -615,7 +556,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate artifact attestation
|
- name: Generate artifact attestation
|
||||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||||
uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0
|
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
|||||||
+80
-80
@@ -40,9 +40,8 @@ env:
|
|||||||
CACHE_VERSION: 3
|
CACHE_VERSION: 3
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 1
|
MYPY_CACHE_VERSION: 1
|
||||||
HA_SHORT_VERSION: "2026.4"
|
HA_SHORT_VERSION: "2026.5"
|
||||||
DEFAULT_PYTHON: "3.14.2"
|
ADDITIONAL_PYTHON_VERSIONS: "[]"
|
||||||
ALL_PYTHON_VERSIONS: "['3.14.2']"
|
|
||||||
# 10.3 is the oldest supported version
|
# 10.3 is the oldest supported version
|
||||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||||
# 10.6 is the current long-term-support
|
# 10.6 is the current long-term-support
|
||||||
@@ -121,7 +120,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "key=$(lsb_release -rs)-apt-${CACHE_VERSION}-${HA_SHORT_VERSION}" >> $GITHUB_OUTPUT
|
echo "key=$(lsb_release -rs)-apt-${CACHE_VERSION}-${HA_SHORT_VERSION}" >> $GITHUB_OUTPUT
|
||||||
- name: Filter for core changes
|
- name: Filter for core changes
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||||
id: core
|
id: core
|
||||||
with:
|
with:
|
||||||
filters: .core_files.yaml
|
filters: .core_files.yaml
|
||||||
@@ -136,7 +135,7 @@ jobs:
|
|||||||
echo "Result:"
|
echo "Result:"
|
||||||
cat .integration_paths.yaml
|
cat .integration_paths.yaml
|
||||||
- name: Filter for integration changes
|
- name: Filter for integration changes
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||||
id: integrations
|
id: integrations
|
||||||
with:
|
with:
|
||||||
filters: .integration_paths.yaml
|
filters: .integration_paths.yaml
|
||||||
@@ -166,6 +165,11 @@ jobs:
|
|||||||
tests_glob=""
|
tests_glob=""
|
||||||
lint_only=""
|
lint_only=""
|
||||||
skip_coverage=""
|
skip_coverage=""
|
||||||
|
default_python=$(cat .python-version)
|
||||||
|
all_python_versions=$(jq -cn \
|
||||||
|
--arg default_python "${default_python}" \
|
||||||
|
--argjson additional_python_versions "${ADDITIONAL_PYTHON_VERSIONS}" \
|
||||||
|
'[$default_python] + $additional_python_versions')
|
||||||
|
|
||||||
if [[ "${INTEGRATION_CHANGES}" != "[]" ]];
|
if [[ "${INTEGRATION_CHANGES}" != "[]" ]];
|
||||||
then
|
then
|
||||||
@@ -235,8 +239,8 @@ jobs:
|
|||||||
echo "mariadb_groups=${mariadb_groups}" >> $GITHUB_OUTPUT
|
echo "mariadb_groups=${mariadb_groups}" >> $GITHUB_OUTPUT
|
||||||
echo "postgresql_groups: ${postgresql_groups}"
|
echo "postgresql_groups: ${postgresql_groups}"
|
||||||
echo "postgresql_groups=${postgresql_groups}" >> $GITHUB_OUTPUT
|
echo "postgresql_groups=${postgresql_groups}" >> $GITHUB_OUTPUT
|
||||||
echo "python_versions: ${ALL_PYTHON_VERSIONS}"
|
echo "python_versions: ${all_python_versions}"
|
||||||
echo "python_versions=${ALL_PYTHON_VERSIONS}" >> $GITHUB_OUTPUT
|
echo "python_versions=${all_python_versions}" >> $GITHUB_OUTPUT
|
||||||
echo "test_full_suite: ${test_full_suite}"
|
echo "test_full_suite: ${test_full_suite}"
|
||||||
echo "test_full_suite=${test_full_suite}" >> $GITHUB_OUTPUT
|
echo "test_full_suite=${test_full_suite}" >> $GITHUB_OUTPUT
|
||||||
echo "integrations_glob: ${integrations_glob}"
|
echo "integrations_glob: ${integrations_glob}"
|
||||||
@@ -276,7 +280,7 @@ jobs:
|
|||||||
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
|
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
|
||||||
echo "::add-matcher::.github/workflows/matchers/codespell.json"
|
echo "::add-matcher::.github/workflows/matchers/codespell.json"
|
||||||
- name: Run prek
|
- name: Run prek
|
||||||
uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # v1.1.1
|
uses: j178/prek-action@53276d8b0d10f8b6672aa85b4588c6921d0370cc # v2.0.1
|
||||||
env:
|
env:
|
||||||
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
|
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
|
||||||
RUFF_OUTPUT_FORMAT: github
|
RUFF_OUTPUT_FORMAT: github
|
||||||
@@ -297,7 +301,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Run zizmor
|
- name: Run zizmor
|
||||||
uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # v1.1.1
|
uses: j178/prek-action@53276d8b0d10f8b6672aa85b4588c6921d0370cc # v2.0.1
|
||||||
with:
|
with:
|
||||||
extra-args: --all-files zizmor
|
extra-args: --all-files zizmor
|
||||||
|
|
||||||
@@ -360,7 +364,7 @@ jobs:
|
|||||||
echo "key=uv-${UV_CACHE_VERSION}-${uv_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
echo "key=uv-${UV_CACHE_VERSION}-${uv_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: >-
|
key: >-
|
||||||
@@ -368,7 +372,7 @@ jobs:
|
|||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Restore uv wheel cache
|
- name: Restore uv wheel cache
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: ${{ env.UV_CACHE_DIR }}
|
path: ${{ env.UV_CACHE_DIR }}
|
||||||
key: >-
|
key: >-
|
||||||
@@ -380,7 +384,7 @@ jobs:
|
|||||||
env.HA_SHORT_VERSION }}-
|
env.HA_SHORT_VERSION }}-
|
||||||
- name: Check if apt cache exists
|
- name: Check if apt cache exists
|
||||||
id: cache-apt-check
|
id: cache-apt-check
|
||||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
|
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
|
||||||
path: |
|
path: |
|
||||||
@@ -426,7 +430,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: Save apt cache
|
- name: Save apt cache
|
||||||
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -452,7 +456,7 @@ jobs:
|
|||||||
python --version
|
python --version
|
||||||
uv pip freeze >> pip_freeze.txt
|
uv pip freeze >> pip_freeze.txt
|
||||||
- name: Upload pip_freeze artifact
|
- name: Upload pip_freeze artifact
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: pip-freeze-${{ matrix.python-version }}
|
name: pip-freeze-${{ matrix.python-version }}
|
||||||
path: pip_freeze.txt
|
path: pip_freeze.txt
|
||||||
@@ -480,7 +484,7 @@ jobs:
|
|||||||
&& github.event.inputs.audit-licenses-only != 'true'
|
&& github.event.inputs.audit-licenses-only != 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -503,15 +507,15 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version-file: ".python-version"
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -540,15 +544,15 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version-file: ".python-version"
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -576,11 +580,11 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version-file: ".python-version"
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Run gen_copilot_instructions.py
|
- name: Run gen_copilot_instructions.py
|
||||||
run: |
|
run: |
|
||||||
@@ -605,7 +609,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Dependency review
|
- name: Dependency review
|
||||||
uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
|
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
|
||||||
with:
|
with:
|
||||||
license-check: false # We use our own license audit checks
|
license-check: false # We use our own license audit checks
|
||||||
|
|
||||||
@@ -639,7 +643,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -653,7 +657,7 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python -m script.licenses extract --output-file=licenses-${PYTHON_VERSION}.json
|
python -m script.licenses extract --output-file=licenses-${PYTHON_VERSION}.json
|
||||||
- name: Upload licenses
|
- name: Upload licenses
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
|
name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
|
||||||
path: licenses-${{ matrix.python-version }}.json
|
path: licenses-${{ matrix.python-version }}.json
|
||||||
@@ -682,15 +686,15 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version-file: ".python-version"
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -735,15 +739,15 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version-file: ".python-version"
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -786,11 +790,11 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version-file: ".python-version"
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Generate partial mypy restore key
|
- name: Generate partial mypy restore key
|
||||||
id: generate-mypy-key
|
id: generate-mypy-key
|
||||||
@@ -798,9 +802,9 @@ jobs:
|
|||||||
mypy_version=$(cat requirements_test.txt | grep 'mypy.*=' | cut -d '=' -f 3)
|
mypy_version=$(cat requirements_test.txt | grep 'mypy.*=' | cut -d '=' -f 3)
|
||||||
echo "version=${mypy_version}" >> $GITHUB_OUTPUT
|
echo "version=${mypy_version}" >> $GITHUB_OUTPUT
|
||||||
echo "key=mypy-${MYPY_CACHE_VERSION}-${mypy_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
echo "key=mypy-${MYPY_CACHE_VERSION}-${mypy_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -808,7 +812,7 @@ jobs:
|
|||||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Restore mypy cache
|
- name: Restore mypy cache
|
||||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: .mypy_cache
|
path: .mypy_cache
|
||||||
key: >-
|
key: >-
|
||||||
@@ -848,13 +852,9 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- info
|
- info
|
||||||
- base
|
- base
|
||||||
- gen-requirements-all
|
|
||||||
- hassfest
|
|
||||||
- prek
|
|
||||||
- mypy
|
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -879,15 +879,15 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version-file: ".python-version"
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -901,7 +901,7 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python -m script.split_tests ${TEST_GROUP_COUNT} tests
|
python -m script.split_tests ${TEST_GROUP_COUNT} tests
|
||||||
- name: Upload pytest_buckets
|
- name: Upload pytest_buckets
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: pytest_buckets
|
name: pytest_buckets
|
||||||
path: pytest_buckets.txt
|
path: pytest_buckets.txt
|
||||||
@@ -930,7 +930,7 @@ jobs:
|
|||||||
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -964,7 +964,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -978,7 +978,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||||
- name: Download pytest_buckets
|
- name: Download pytest_buckets
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: pytest_buckets
|
name: pytest_buckets
|
||||||
- name: Compile English translations
|
- name: Compile English translations
|
||||||
@@ -1020,14 +1020,14 @@ jobs:
|
|||||||
2>&1 | tee pytest-${PYTHON_VERSION}-${TEST_GROUP}.txt
|
2>&1 | tee pytest-${PYTHON_VERSION}-${TEST_GROUP}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: pytest-*.txt
|
path: pytest-*.txt
|
||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
@@ -1040,7 +1040,7 @@ jobs:
|
|||||||
mv "junit.xml-tmp" "junit.xml"
|
mv "junit.xml-tmp" "junit.xml"
|
||||||
- name: Upload test results artifact
|
- name: Upload test results artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: test-results-full-${{ matrix.python-version }}-${{ matrix.group }}
|
name: test-results-full-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: junit.xml
|
path: junit.xml
|
||||||
@@ -1080,7 +1080,7 @@ jobs:
|
|||||||
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
|
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -1115,7 +1115,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1177,7 +1177,7 @@ jobs:
|
|||||||
2>&1 | tee pytest-${PYTHON_VERSION}-${mariadb}.txt
|
2>&1 | tee pytest-${PYTHON_VERSION}-${mariadb}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.mariadb }}
|
steps.pytest-partial.outputs.mariadb }}
|
||||||
@@ -1185,7 +1185,7 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{
|
name: coverage-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.mariadb }}
|
steps.pytest-partial.outputs.mariadb }}
|
||||||
@@ -1199,7 +1199,7 @@ jobs:
|
|||||||
mv "junit.xml-tmp" "junit.xml"
|
mv "junit.xml-tmp" "junit.xml"
|
||||||
- name: Upload test results artifact
|
- name: Upload test results artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: test-results-mariadb-${{ matrix.python-version }}-${{
|
name: test-results-mariadb-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.mariadb }}
|
steps.pytest-partial.outputs.mariadb }}
|
||||||
@@ -1238,7 +1238,7 @@ jobs:
|
|||||||
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
|
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -1275,7 +1275,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1338,7 +1338,7 @@ jobs:
|
|||||||
2>&1 | tee pytest-${PYTHON_VERSION}-${postgresql}.txt
|
2>&1 | tee pytest-${PYTHON_VERSION}-${postgresql}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.postgresql }}
|
steps.pytest-partial.outputs.postgresql }}
|
||||||
@@ -1346,7 +1346,7 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{
|
name: coverage-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.postgresql }}
|
steps.pytest-partial.outputs.postgresql }}
|
||||||
@@ -1360,7 +1360,7 @@ jobs:
|
|||||||
mv "junit.xml-tmp" "junit.xml"
|
mv "junit.xml-tmp" "junit.xml"
|
||||||
- name: Upload test results artifact
|
- name: Upload test results artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: test-results-postgres-${{ matrix.python-version }}-${{
|
name: test-results-postgres-${{ matrix.python-version }}-${{
|
||||||
steps.pytest-partial.outputs.postgresql }}
|
steps.pytest-partial.outputs.postgresql }}
|
||||||
@@ -1387,16 +1387,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'true'
|
if: needs.info.outputs.test_full_suite == 'true'
|
||||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
flags: full-suite
|
flags: full-suite
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
|
||||||
|
|
||||||
pytest-partial:
|
pytest-partial:
|
||||||
name: Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
|
name: Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
|
||||||
@@ -1421,7 +1421,7 @@ jobs:
|
|||||||
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -1455,7 +1455,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1514,14 +1514,14 @@ jobs:
|
|||||||
2>&1 | tee pytest-${PYTHON_VERSION}-${TEST_GROUP}.txt
|
2>&1 | tee pytest-${PYTHON_VERSION}-${TEST_GROUP}.txt
|
||||||
- name: Upload pytest output
|
- name: Upload pytest output
|
||||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: pytest-*.txt
|
path: pytest-*.txt
|
||||||
overwrite: true
|
overwrite: true
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true'
|
if: needs.info.outputs.skip_coverage != 'true'
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
@@ -1534,7 +1534,7 @@ jobs:
|
|||||||
mv "junit.xml-tmp" "junit.xml"
|
mv "junit.xml-tmp" "junit.xml"
|
||||||
- name: Upload test results artifact
|
- name: Upload test results artifact
|
||||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: test-results-partial-${{ matrix.python-version }}-${{ matrix.group }}
|
name: test-results-partial-${{ matrix.python-version }}-${{ matrix.group }}
|
||||||
path: junit.xml
|
path: junit.xml
|
||||||
@@ -1558,15 +1558,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'false'
|
if: needs.info.outputs.test_full_suite == 'false'
|
||||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
|
||||||
|
|
||||||
upload-test-results:
|
upload-test-results:
|
||||||
name: Upload test results to Codecov
|
name: Upload test results to Codecov
|
||||||
@@ -1587,11 +1587,11 @@ jobs:
|
|||||||
&& needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
&& needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||||
steps:
|
steps:
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
pattern: test-results-*
|
pattern: test-results-*
|
||||||
- name: Upload test results to Codecov
|
- name: Upload test results to Codecov
|
||||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
||||||
with:
|
with:
|
||||||
report_type: test_results
|
report_type: test_results
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ jobs:
|
|||||||
- name: Detect duplicates using AI
|
- name: Detect duplicates using AI
|
||||||
id: ai_detection
|
id: ai_detection
|
||||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||||
uses: actions/ai-inference@a380166897b5408b8fb7dddd148142794cb5624a # v2.0.6
|
uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o
|
model: openai/gpt-4o
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
- name: Detect language using AI
|
- name: Detect language using AI
|
||||||
id: ai_language_detection
|
id: ai_language_detection
|
||||||
if: steps.detect_language.outputs.should_continue == 'true'
|
if: steps.detect_language.outputs.should_continue == 'true'
|
||||||
uses: actions/ai-inference@a380166897b5408b8fb7dddd148142794cb5624a # v2.0.6
|
uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o-mini
|
model: openai/gpt-4o-mini
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ jobs:
|
|||||||
# v1.7.0
|
# v1.7.0
|
||||||
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a
|
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.ISSUE_TRIAGE_APP_ID }}
|
app_id: ${{ secrets.ISSUE_TRIAGE_APP_ID }} # zizmor: ignore[secrets-outside-env]
|
||||||
private_key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }}
|
private_key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }} # zizmor: ignore[secrets-outside-env]
|
||||||
|
|
||||||
# The 90 day stale policy for issues
|
# The 90 day stale policy for issues
|
||||||
# Used for:
|
# Used for:
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}
|
group: ${{ github.workflow }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
|
||||||
DEFAULT_PYTHON: "3.14.2"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
upload:
|
upload:
|
||||||
name: Upload
|
name: Upload
|
||||||
@@ -29,13 +26,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version-file: ".python-version"
|
||||||
|
|
||||||
- name: Upload Translations
|
- name: Upload Translations
|
||||||
env:
|
env:
|
||||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
|
||||||
run: |
|
run: |
|
||||||
python3 -m script.translations upload
|
python3 -m script.translations upload
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ on:
|
|||||||
- "requirements.txt"
|
- "requirements.txt"
|
||||||
- "script/gen_requirements_all.py"
|
- "script/gen_requirements_all.py"
|
||||||
|
|
||||||
env:
|
|
||||||
DEFAULT_PYTHON: "3.14.2"
|
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -36,11 +33,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version-file: ".python-version"
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Create Python virtual environment
|
- name: Create Python virtual environment
|
||||||
@@ -77,7 +74,7 @@ jobs:
|
|||||||
) > .env_file
|
) > .env_file
|
||||||
|
|
||||||
- name: Upload env_file
|
- name: Upload env_file
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
path: ./.env_file
|
path: ./.env_file
|
||||||
@@ -85,7 +82,7 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
|
|
||||||
- name: Upload requirements_diff
|
- name: Upload requirements_diff
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
path: ./requirements_diff.txt
|
path: ./requirements_diff.txt
|
||||||
@@ -97,7 +94,7 @@ jobs:
|
|||||||
python -m script.gen_requirements_all ci
|
python -m script.gen_requirements_all ci
|
||||||
|
|
||||||
- name: Upload requirements_all_wheels
|
- name: Upload requirements_all_wheels
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: requirements_all_wheels
|
name: requirements_all_wheels
|
||||||
path: ./requirements_all_wheels_*.txt
|
path: ./requirements_all_wheels_*.txt
|
||||||
@@ -124,12 +121,12 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
- name: Download requirements_diff
|
- name: Download requirements_diff
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
@@ -145,7 +142,7 @@ jobs:
|
|||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }} # zizmor: ignore[secrets-outside-env]
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-ng-dev"
|
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-ng-dev"
|
||||||
skip-binary: aiohttp;multidict;propcache;yarl;SQLAlchemy
|
skip-binary: aiohttp;multidict;propcache;yarl;SQLAlchemy
|
||||||
@@ -175,17 +172,17 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
- name: Download requirements_diff
|
- name: Download requirements_diff
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
- name: Download requirements_all_wheels
|
- name: Download requirements_all_wheels
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: requirements_all_wheels
|
name: requirements_all_wheels
|
||||||
|
|
||||||
@@ -203,10 +200,10 @@ jobs:
|
|||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }} # zizmor: ignore[secrets-outside-env]
|
||||||
env-file: true
|
env-file: true
|
||||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev"
|
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev"
|
||||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
requirements: "requirements_all.txt"
|
requirements: "requirements_all_wheels_${{ matrix.arch }}.txt"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ repos:
|
|||||||
exclude_types: [csv, json, html]
|
exclude_types: [csv, json, html]
|
||||||
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/
|
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/
|
||||||
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
||||||
rev: v1.22.0
|
rev: v1.23.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: zizmor
|
- id: zizmor
|
||||||
args:
|
args:
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.14
|
3.14.2
|
||||||
|
|||||||
+8
-1
@@ -123,7 +123,6 @@ homeassistant.components.blueprint.*
|
|||||||
homeassistant.components.bluesound.*
|
homeassistant.components.bluesound.*
|
||||||
homeassistant.components.bluetooth.*
|
homeassistant.components.bluetooth.*
|
||||||
homeassistant.components.bluetooth_adapters.*
|
homeassistant.components.bluetooth_adapters.*
|
||||||
homeassistant.components.bmw_connected_drive.*
|
|
||||||
homeassistant.components.bond.*
|
homeassistant.components.bond.*
|
||||||
homeassistant.components.bosch_alarm.*
|
homeassistant.components.bosch_alarm.*
|
||||||
homeassistant.components.braviatv.*
|
homeassistant.components.braviatv.*
|
||||||
@@ -138,6 +137,7 @@ homeassistant.components.calendar.*
|
|||||||
homeassistant.components.cambridge_audio.*
|
homeassistant.components.cambridge_audio.*
|
||||||
homeassistant.components.camera.*
|
homeassistant.components.camera.*
|
||||||
homeassistant.components.canary.*
|
homeassistant.components.canary.*
|
||||||
|
homeassistant.components.casper_glow.*
|
||||||
homeassistant.components.cert_expiry.*
|
homeassistant.components.cert_expiry.*
|
||||||
homeassistant.components.clickatell.*
|
homeassistant.components.clickatell.*
|
||||||
homeassistant.components.clicksend.*
|
homeassistant.components.clicksend.*
|
||||||
@@ -213,6 +213,7 @@ homeassistant.components.flexit_bacnet.*
|
|||||||
homeassistant.components.flux_led.*
|
homeassistant.components.flux_led.*
|
||||||
homeassistant.components.folder_watcher.*
|
homeassistant.components.folder_watcher.*
|
||||||
homeassistant.components.forecast_solar.*
|
homeassistant.components.forecast_solar.*
|
||||||
|
homeassistant.components.freshr.*
|
||||||
homeassistant.components.fritz.*
|
homeassistant.components.fritz.*
|
||||||
homeassistant.components.fritzbox.*
|
homeassistant.components.fritzbox.*
|
||||||
homeassistant.components.fritzbox_callmonitor.*
|
homeassistant.components.fritzbox_callmonitor.*
|
||||||
@@ -272,10 +273,12 @@ homeassistant.components.homekit_controller.storage
|
|||||||
homeassistant.components.homekit_controller.utils
|
homeassistant.components.homekit_controller.utils
|
||||||
homeassistant.components.homewizard.*
|
homeassistant.components.homewizard.*
|
||||||
homeassistant.components.homeworks.*
|
homeassistant.components.homeworks.*
|
||||||
|
homeassistant.components.hr_energy_qube.*
|
||||||
homeassistant.components.http.*
|
homeassistant.components.http.*
|
||||||
homeassistant.components.huawei_lte.*
|
homeassistant.components.huawei_lte.*
|
||||||
homeassistant.components.humidifier.*
|
homeassistant.components.humidifier.*
|
||||||
homeassistant.components.husqvarna_automower.*
|
homeassistant.components.husqvarna_automower.*
|
||||||
|
homeassistant.components.huum.*
|
||||||
homeassistant.components.hydrawise.*
|
homeassistant.components.hydrawise.*
|
||||||
homeassistant.components.hyperion.*
|
homeassistant.components.hyperion.*
|
||||||
homeassistant.components.hypontech.*
|
homeassistant.components.hypontech.*
|
||||||
@@ -325,6 +328,7 @@ homeassistant.components.ld2410_ble.*
|
|||||||
homeassistant.components.led_ble.*
|
homeassistant.components.led_ble.*
|
||||||
homeassistant.components.lektrico.*
|
homeassistant.components.lektrico.*
|
||||||
homeassistant.components.letpot.*
|
homeassistant.components.letpot.*
|
||||||
|
homeassistant.components.lg_infrared.*
|
||||||
homeassistant.components.libre_hardware_monitor.*
|
homeassistant.components.libre_hardware_monitor.*
|
||||||
homeassistant.components.lidarr.*
|
homeassistant.components.lidarr.*
|
||||||
homeassistant.components.lifx.*
|
homeassistant.components.lifx.*
|
||||||
@@ -342,6 +346,7 @@ homeassistant.components.lookin.*
|
|||||||
homeassistant.components.lovelace.*
|
homeassistant.components.lovelace.*
|
||||||
homeassistant.components.luftdaten.*
|
homeassistant.components.luftdaten.*
|
||||||
homeassistant.components.lunatone.*
|
homeassistant.components.lunatone.*
|
||||||
|
homeassistant.components.lutron.*
|
||||||
homeassistant.components.madvr.*
|
homeassistant.components.madvr.*
|
||||||
homeassistant.components.manual.*
|
homeassistant.components.manual.*
|
||||||
homeassistant.components.mastodon.*
|
homeassistant.components.mastodon.*
|
||||||
@@ -569,9 +574,11 @@ homeassistant.components.trafikverket_train.*
|
|||||||
homeassistant.components.trafikverket_weatherstation.*
|
homeassistant.components.trafikverket_weatherstation.*
|
||||||
homeassistant.components.transmission.*
|
homeassistant.components.transmission.*
|
||||||
homeassistant.components.trend.*
|
homeassistant.components.trend.*
|
||||||
|
homeassistant.components.trmnl.*
|
||||||
homeassistant.components.tts.*
|
homeassistant.components.tts.*
|
||||||
homeassistant.components.twentemilieu.*
|
homeassistant.components.twentemilieu.*
|
||||||
homeassistant.components.unifi.*
|
homeassistant.components.unifi.*
|
||||||
|
homeassistant.components.unifi_access.*
|
||||||
homeassistant.components.unifiprotect.*
|
homeassistant.components.unifiprotect.*
|
||||||
homeassistant.components.upcloud.*
|
homeassistant.components.upcloud.*
|
||||||
homeassistant.components.update.*
|
homeassistant.components.update.*
|
||||||
|
|||||||
@@ -4,325 +4,22 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
|||||||
|
|
||||||
## Code Review Guidelines
|
## Code Review Guidelines
|
||||||
|
|
||||||
**When reviewing code, do NOT comment on:**
|
|
||||||
- **Missing imports** - We use static analysis tooling to catch that
|
|
||||||
- **Code formatting** - We have ruff as a formatting tool that will catch those if needed (unless specifically instructed otherwise in these instructions)
|
|
||||||
|
|
||||||
**Git commit practices during review:**
|
**Git commit practices during review:**
|
||||||
- **Do NOT amend, squash, or rebase commits after review has started** - Reviewers need to see what changed since their last review
|
- **Do NOT amend, squash, or rebase commits after review has started** - Reviewers need to see what changed since their last review
|
||||||
|
|
||||||
## Python Requirements
|
|
||||||
|
|
||||||
- **Compatibility**: Python 3.13+
|
|
||||||
- **Language Features**: Use the newest features when possible:
|
|
||||||
- Pattern matching
|
|
||||||
- Type hints
|
|
||||||
- f-strings (preferred over `%` or `.format()`)
|
|
||||||
- Dataclasses
|
|
||||||
- Walrus operator
|
|
||||||
|
|
||||||
### Strict Typing (Platinum)
|
|
||||||
- **Comprehensive Type Hints**: Add type hints to all functions, methods, and variables
|
|
||||||
- **Custom Config Entry Types**: When using runtime_data:
|
|
||||||
```python
|
|
||||||
type MyIntegrationConfigEntry = ConfigEntry[MyClient]
|
|
||||||
```
|
|
||||||
- **Library Requirements**: Include `py.typed` file for PEP-561 compliance
|
|
||||||
|
|
||||||
## Code Quality Standards
|
|
||||||
|
|
||||||
- **Formatting**: Ruff
|
|
||||||
- **Linting**: PyLint and Ruff
|
|
||||||
- **Type Checking**: MyPy
|
|
||||||
- **Lint/Type/Format Fixes**: Always prefer addressing the underlying issue (e.g., import the typed source, update shared stubs, align with Ruff expectations, or correct formatting at the source) before disabling a rule, adding `# type: ignore`, or skipping a formatter. Treat suppressions and `noqa` comments as a last resort once no compliant fix exists
|
|
||||||
- **Testing**: pytest with plain functions and fixtures
|
|
||||||
- **Language**: American English for all code, comments, and documentation (use sentence case, including titles)
|
|
||||||
|
|
||||||
### Writing Style Guidelines
|
|
||||||
- **Tone**: Friendly and informative
|
|
||||||
- **Perspective**: Use second-person ("you" and "your") for user-facing messages
|
|
||||||
- **Inclusivity**: Use objective, non-discriminatory language
|
|
||||||
- **Clarity**: Write for non-native English speakers
|
|
||||||
- **Formatting in Messages**:
|
|
||||||
- Use backticks for: file paths, filenames, variable names, field entries
|
|
||||||
- Use sentence case for titles and messages (capitalize only the first word and proper nouns)
|
|
||||||
- Avoid abbreviations when possible
|
|
||||||
|
|
||||||
### Documentation Standards
|
|
||||||
- **File Headers**: Short and concise
|
|
||||||
```python
|
|
||||||
"""Integration for Peblar EV chargers."""
|
|
||||||
```
|
|
||||||
- **Method/Function Docstrings**: Required for all
|
|
||||||
```python
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: PeblarConfigEntry) -> bool:
|
|
||||||
"""Set up Peblar from a config entry."""
|
|
||||||
```
|
|
||||||
- **Comment Style**:
|
|
||||||
- Use clear, descriptive comments
|
|
||||||
- Explain the "why" not just the "what"
|
|
||||||
- Keep code block lines under 80 characters when possible
|
|
||||||
- Use progressive disclosure (simple explanation first, complex details later)
|
|
||||||
|
|
||||||
## Async Programming
|
|
||||||
|
|
||||||
- All external I/O operations must be async
|
|
||||||
- **Best Practices**:
|
|
||||||
- Avoid sleeping in loops
|
|
||||||
- Avoid awaiting in loops - use `gather` instead
|
|
||||||
- No blocking calls
|
|
||||||
- Group executor jobs when possible - switching between event loop and executor is expensive
|
|
||||||
|
|
||||||
### Blocking Operations
|
|
||||||
- **Use Executor**: For blocking I/O operations
|
|
||||||
```python
|
|
||||||
result = await hass.async_add_executor_job(blocking_function, args)
|
|
||||||
```
|
|
||||||
- **Never Block Event Loop**: Avoid file operations, `time.sleep()`, blocking HTTP calls
|
|
||||||
- **Replace with Async**: Use `asyncio.sleep()` instead of `time.sleep()`
|
|
||||||
|
|
||||||
### Thread Safety
|
|
||||||
- **@callback Decorator**: For event loop safe functions
|
|
||||||
```python
|
|
||||||
@callback
|
|
||||||
def async_update_callback(self, event):
|
|
||||||
"""Safe to run in event loop."""
|
|
||||||
self.async_write_ha_state()
|
|
||||||
```
|
|
||||||
- **Sync APIs from Threads**: Use sync versions when calling from non-event loop threads
|
|
||||||
- **Registry Changes**: Must be done in event loop thread
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
- **Exception Types**: Choose most specific exception available
|
|
||||||
- `ServiceValidationError`: User input errors (preferred over `ValueError`)
|
|
||||||
- `HomeAssistantError`: Device communication failures
|
|
||||||
- `ConfigEntryNotReady`: Temporary setup issues (device offline)
|
|
||||||
- `ConfigEntryAuthFailed`: Authentication problems
|
|
||||||
- `ConfigEntryError`: Permanent setup issues
|
|
||||||
- **Try/Catch Best Practices**:
|
|
||||||
- Only wrap code that can throw exceptions
|
|
||||||
- Keep try blocks minimal - process data after the try/catch
|
|
||||||
- **Avoid bare exceptions** except in specific cases:
|
|
||||||
- ❌ Generally not allowed: `except:` or `except Exception:`
|
|
||||||
- ✅ Allowed in config flows to ensure robustness
|
|
||||||
- ✅ Allowed in functions/methods that run in background tasks
|
|
||||||
- Bad pattern:
|
|
||||||
```python
|
|
||||||
try:
|
|
||||||
data = await device.get_data() # Can throw
|
|
||||||
# ❌ Don't process data inside try block
|
|
||||||
processed = data.get("value", 0) * 100
|
|
||||||
self._attr_native_value = processed
|
|
||||||
except DeviceError:
|
|
||||||
_LOGGER.error("Failed to get data")
|
|
||||||
```
|
|
||||||
- Good pattern:
|
|
||||||
```python
|
|
||||||
try:
|
|
||||||
data = await device.get_data() # Can throw
|
|
||||||
except DeviceError:
|
|
||||||
_LOGGER.error("Failed to get data")
|
|
||||||
return
|
|
||||||
|
|
||||||
# ✅ Process data outside try block
|
|
||||||
processed = data.get("value", 0) * 100
|
|
||||||
self._attr_native_value = processed
|
|
||||||
```
|
|
||||||
- **Bare Exception Usage**:
|
|
||||||
```python
|
|
||||||
# ❌ Not allowed in regular code
|
|
||||||
try:
|
|
||||||
data = await device.get_data()
|
|
||||||
except Exception: # Too broad
|
|
||||||
_LOGGER.error("Failed")
|
|
||||||
|
|
||||||
# ✅ Allowed in config flow for robustness
|
|
||||||
async def async_step_user(self, user_input=None):
|
|
||||||
try:
|
|
||||||
await self._test_connection(user_input)
|
|
||||||
except Exception: # Allowed here
|
|
||||||
errors["base"] = "unknown"
|
|
||||||
|
|
||||||
# ✅ Allowed in background tasks
|
|
||||||
async def _background_refresh():
|
|
||||||
try:
|
|
||||||
await coordinator.async_refresh()
|
|
||||||
except Exception: # Allowed in task
|
|
||||||
_LOGGER.exception("Unexpected error in background task")
|
|
||||||
```
|
|
||||||
- **Setup Failure Patterns**:
|
|
||||||
```python
|
|
||||||
try:
|
|
||||||
await device.async_setup()
|
|
||||||
except (asyncio.TimeoutError, TimeoutException) as ex:
|
|
||||||
raise ConfigEntryNotReady(f"Timeout connecting to {device.host}") from ex
|
|
||||||
except AuthFailed as ex:
|
|
||||||
raise ConfigEntryAuthFailed(f"Credentials expired for {device.name}") from ex
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logging
|
|
||||||
- **Format Guidelines**:
|
|
||||||
- No periods at end of messages
|
|
||||||
- No integration names/domains (added automatically)
|
|
||||||
- No sensitive data (keys, tokens, passwords)
|
|
||||||
- Use debug level for non-user-facing messages
|
|
||||||
- **Use Lazy Logging**:
|
|
||||||
```python
|
|
||||||
_LOGGER.debug("This is a log message with %s", variable)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Unavailability Logging
|
|
||||||
- **Log Once**: When device/service becomes unavailable (info level)
|
|
||||||
- **Log Recovery**: When device/service comes back online
|
|
||||||
- **Implementation Pattern**:
|
|
||||||
```python
|
|
||||||
_unavailable_logged: bool = False
|
|
||||||
|
|
||||||
if not self._unavailable_logged:
|
|
||||||
_LOGGER.info("The sensor is unavailable: %s", ex)
|
|
||||||
self._unavailable_logged = True
|
|
||||||
# On recovery:
|
|
||||||
if self._unavailable_logged:
|
|
||||||
_LOGGER.info("The sensor is back online")
|
|
||||||
self._unavailable_logged = False
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
### Environment
|
.vscode/tasks.json contains useful commands used for development.
|
||||||
- **Local development (non-container)**: Activate the project venv before running commands: `source .venv/bin/activate`
|
|
||||||
- **Dev container**: No activation needed, the environment is pre-configured
|
|
||||||
|
|
||||||
### Code Quality & Linting
|
## Python Syntax Notes
|
||||||
- **Run all linters on all files**: `prek run --all-files`
|
|
||||||
- **Run linters on staged files only**: `prek run`
|
|
||||||
- **PyLint on everything** (slow): `pylint homeassistant`
|
|
||||||
- **PyLint on specific folder**: `pylint homeassistant/components/my_integration`
|
|
||||||
- **MyPy type checking (whole project)**: `mypy homeassistant/`
|
|
||||||
- **MyPy on specific integration**: `mypy homeassistant/components/my_integration`
|
|
||||||
|
|
||||||
### Testing
|
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses.
|
||||||
- **Quick test of changed files**: `pytest --timeout=10 --picked`
|
|
||||||
- **Update test snapshots**: Add `--snapshot-update` to pytest command
|
|
||||||
- ⚠️ Omit test results after using `--snapshot-update`
|
|
||||||
- Always run tests again without the flag to verify snapshots
|
|
||||||
- **Full test suite** (AVOID - very slow): `pytest ./tests`
|
|
||||||
|
|
||||||
### Dependencies & Requirements
|
## Testing
|
||||||
- **Update generated files after dependency changes**: `python -m script.gen_requirements_all`
|
|
||||||
- **Install all Python requirements**:
|
|
||||||
```bash
|
|
||||||
uv pip install -r requirements_all.txt -r requirements.txt -r requirements_test.txt
|
|
||||||
```
|
|
||||||
- **Install test requirements only**:
|
|
||||||
```bash
|
|
||||||
uv pip install -r requirements_test_all.txt -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### Translations
|
When writing or modifying tests, ensure all test function parameters have type annotations.
|
||||||
- **Update translations after strings.json changes**:
|
Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) over `Any`.
|
||||||
```bash
|
|
||||||
python -m script.translations develop --all
|
|
||||||
```
|
|
||||||
|
|
||||||
### Project Validation
|
## Good practices
|
||||||
- **Run hassfest** (checks project structure and updates generated files):
|
|
||||||
```bash
|
|
||||||
python -m script.hassfest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Anti-Patterns & Best Practices
|
Integrations with Platinum or Gold level in the Integration Quality Scale reflect a high standard of code quality and maintainability. When looking for examples of something, these are good places to start. The level is indicated in the manifest.json of the integration.
|
||||||
|
|
||||||
### ❌ **Avoid These Patterns**
|
|
||||||
```python
|
|
||||||
# Blocking operations in event loop
|
|
||||||
data = requests.get(url) # ❌ Blocks event loop
|
|
||||||
time.sleep(5) # ❌ Blocks event loop
|
|
||||||
|
|
||||||
# Reusing BleakClient instances
|
|
||||||
self.client = BleakClient(address)
|
|
||||||
await self.client.connect()
|
|
||||||
# Later...
|
|
||||||
await self.client.connect() # ❌ Don't reuse
|
|
||||||
|
|
||||||
# Hardcoded strings in code
|
|
||||||
self._attr_name = "Temperature Sensor" # ❌ Not translatable
|
|
||||||
|
|
||||||
# Missing error handling
|
|
||||||
data = await self.api.get_data() # ❌ No exception handling
|
|
||||||
|
|
||||||
# Storing sensitive data in diagnostics
|
|
||||||
return {"api_key": entry.data[CONF_API_KEY]} # ❌ Exposes secrets
|
|
||||||
|
|
||||||
# Accessing hass.data directly in tests
|
|
||||||
coordinator = hass.data[DOMAIN][entry.entry_id] # ❌ Don't access hass.data
|
|
||||||
|
|
||||||
# User-configurable polling intervals
|
|
||||||
# In config flow
|
|
||||||
vol.Optional("scan_interval", default=60): cv.positive_int # ❌ Not allowed
|
|
||||||
# In coordinator
|
|
||||||
update_interval = timedelta(minutes=entry.data.get("scan_interval", 1)) # ❌ Not allowed
|
|
||||||
|
|
||||||
# User-configurable config entry names (non-helper integrations)
|
|
||||||
vol.Optional("name", default="My Device"): cv.string # ❌ Not allowed in regular integrations
|
|
||||||
|
|
||||||
# Too much code in try block
|
|
||||||
try:
|
|
||||||
response = await client.get_data() # Can throw
|
|
||||||
# ❌ Data processing should be outside try block
|
|
||||||
temperature = response["temperature"] / 10
|
|
||||||
humidity = response["humidity"]
|
|
||||||
self._attr_native_value = temperature
|
|
||||||
except ClientError:
|
|
||||||
_LOGGER.error("Failed to fetch data")
|
|
||||||
|
|
||||||
# Bare exceptions in regular code
|
|
||||||
try:
|
|
||||||
value = await sensor.read_value()
|
|
||||||
except Exception: # ❌ Too broad - catch specific exceptions
|
|
||||||
_LOGGER.error("Failed to read sensor")
|
|
||||||
```
|
|
||||||
|
|
||||||
### ✅ **Use These Patterns Instead**
|
|
||||||
```python
|
|
||||||
# Async operations with executor
|
|
||||||
data = await hass.async_add_executor_job(requests.get, url)
|
|
||||||
await asyncio.sleep(5) # ✅ Non-blocking
|
|
||||||
|
|
||||||
# Fresh BleakClient instances
|
|
||||||
client = BleakClient(address) # ✅ New instance each time
|
|
||||||
await client.connect()
|
|
||||||
|
|
||||||
# Translatable entity names
|
|
||||||
_attr_translation_key = "temperature_sensor" # ✅ Translatable
|
|
||||||
|
|
||||||
# Proper error handling
|
|
||||||
try:
|
|
||||||
data = await self.api.get_data()
|
|
||||||
except ApiException as err:
|
|
||||||
raise UpdateFailed(f"API error: {err}") from err
|
|
||||||
|
|
||||||
# Redacted diagnostics data
|
|
||||||
return async_redact_data(data, {"api_key", "password"}) # ✅ Safe
|
|
||||||
|
|
||||||
# Test through proper integration setup and fixtures
|
|
||||||
@pytest.fixture
|
|
||||||
async def init_integration(hass, mock_config_entry, mock_api):
|
|
||||||
mock_config_entry.add_to_hass(hass)
|
|
||||||
await hass.config_entries.async_setup(mock_config_entry.entry_id) # ✅ Proper setup
|
|
||||||
|
|
||||||
# Integration-determined polling intervals (not user-configurable)
|
|
||||||
SCAN_INTERVAL = timedelta(minutes=5) # ✅ Common pattern: constant in const.py
|
|
||||||
|
|
||||||
class MyCoordinator(DataUpdateCoordinator[MyData]):
|
|
||||||
def __init__(self, hass: HomeAssistant, client: MyClient, config_entry: ConfigEntry) -> None:
|
|
||||||
# ✅ Integration determines interval based on device capabilities, connection type, etc.
|
|
||||||
interval = timedelta(minutes=1) if client.is_local else SCAN_INTERVAL
|
|
||||||
super().__init__(
|
|
||||||
hass,
|
|
||||||
logger=LOGGER,
|
|
||||||
name=DOMAIN,
|
|
||||||
update_interval=interval,
|
|
||||||
config_entry=config_entry, # ✅ Pass config_entry - it's accepted and recommended
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|||||||
Generated
+70
-23
@@ -186,6 +186,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/auth/ @home-assistant/core
|
/tests/components/auth/ @home-assistant/core
|
||||||
/homeassistant/components/automation/ @home-assistant/core
|
/homeassistant/components/automation/ @home-assistant/core
|
||||||
/tests/components/automation/ @home-assistant/core
|
/tests/components/automation/ @home-assistant/core
|
||||||
|
/homeassistant/components/autoskope/ @mcisk
|
||||||
|
/tests/components/autoskope/ @mcisk
|
||||||
/homeassistant/components/avea/ @pattyland
|
/homeassistant/components/avea/ @pattyland
|
||||||
/homeassistant/components/awair/ @ahayworth @ricohageman
|
/homeassistant/components/awair/ @ahayworth @ricohageman
|
||||||
/tests/components/awair/ @ahayworth @ricohageman
|
/tests/components/awair/ @ahayworth @ricohageman
|
||||||
@@ -212,14 +214,16 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/balboa/ @garbled1 @natekspencer
|
/tests/components/balboa/ @garbled1 @natekspencer
|
||||||
/homeassistant/components/bang_olufsen/ @mj23000
|
/homeassistant/components/bang_olufsen/ @mj23000
|
||||||
/tests/components/bang_olufsen/ @mj23000
|
/tests/components/bang_olufsen/ @mj23000
|
||||||
|
/homeassistant/components/battery/ @home-assistant/core
|
||||||
|
/tests/components/battery/ @home-assistant/core
|
||||||
/homeassistant/components/bayesian/ @HarvsG
|
/homeassistant/components/bayesian/ @HarvsG
|
||||||
/tests/components/bayesian/ @HarvsG
|
/tests/components/bayesian/ @HarvsG
|
||||||
/homeassistant/components/beewi_smartclim/ @alemuro
|
/homeassistant/components/beewi_smartclim/ @alemuro
|
||||||
/homeassistant/components/binary_sensor/ @home-assistant/core
|
/homeassistant/components/binary_sensor/ @home-assistant/core
|
||||||
/tests/components/binary_sensor/ @home-assistant/core
|
/tests/components/binary_sensor/ @home-assistant/core
|
||||||
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
|
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
|
||||||
/homeassistant/components/blebox/ @bbx-a @swistakm
|
/homeassistant/components/blebox/ @bbx-a @swistakm @bkobus-bbx
|
||||||
/tests/components/blebox/ @bbx-a @swistakm
|
/tests/components/blebox/ @bbx-a @swistakm @bkobus-bbx
|
||||||
/homeassistant/components/blink/ @fronzbot
|
/homeassistant/components/blink/ @fronzbot
|
||||||
/tests/components/blink/ @fronzbot
|
/tests/components/blink/ @fronzbot
|
||||||
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||||
@@ -234,8 +238,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/bluetooth/ @bdraco
|
/tests/components/bluetooth/ @bdraco
|
||||||
/homeassistant/components/bluetooth_adapters/ @bdraco
|
/homeassistant/components/bluetooth_adapters/ @bdraco
|
||||||
/tests/components/bluetooth_adapters/ @bdraco
|
/tests/components/bluetooth_adapters/ @bdraco
|
||||||
/homeassistant/components/bmw_connected_drive/ @gerard33 @rikroe
|
|
||||||
/tests/components/bmw_connected_drive/ @gerard33 @rikroe
|
|
||||||
/homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
|
/homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
|
||||||
/tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
|
/tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
|
||||||
/homeassistant/components/bosch_alarm/ @mag1024 @sanjay900
|
/homeassistant/components/bosch_alarm/ @mag1024 @sanjay900
|
||||||
@@ -273,6 +275,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/cambridge_audio/ @noahhusby
|
/tests/components/cambridge_audio/ @noahhusby
|
||||||
/homeassistant/components/camera/ @home-assistant/core
|
/homeassistant/components/camera/ @home-assistant/core
|
||||||
/tests/components/camera/ @home-assistant/core
|
/tests/components/camera/ @home-assistant/core
|
||||||
|
/homeassistant/components/casper_glow/ @mikeodr
|
||||||
|
/tests/components/casper_glow/ @mikeodr
|
||||||
/homeassistant/components/cast/ @emontnemery
|
/homeassistant/components/cast/ @emontnemery
|
||||||
/tests/components/cast/ @emontnemery
|
/tests/components/cast/ @emontnemery
|
||||||
/homeassistant/components/ccm15/ @ocalvo
|
/homeassistant/components/ccm15/ @ocalvo
|
||||||
@@ -281,6 +285,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/cert_expiry/ @jjlawren
|
/tests/components/cert_expiry/ @jjlawren
|
||||||
/homeassistant/components/chacon_dio/ @cnico
|
/homeassistant/components/chacon_dio/ @cnico
|
||||||
/tests/components/chacon_dio/ @cnico
|
/tests/components/chacon_dio/ @cnico
|
||||||
|
/homeassistant/components/chess_com/ @joostlek
|
||||||
|
/tests/components/chess_com/ @joostlek
|
||||||
/homeassistant/components/cisco_ios/ @fbradyirl
|
/homeassistant/components/cisco_ios/ @fbradyirl
|
||||||
/homeassistant/components/cisco_mobility_express/ @fbradyirl
|
/homeassistant/components/cisco_mobility_express/ @fbradyirl
|
||||||
/homeassistant/components/cisco_webex_teams/ @fbradyirl
|
/homeassistant/components/cisco_webex_teams/ @fbradyirl
|
||||||
@@ -383,6 +389,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/dlna_dms/ @chishm
|
/tests/components/dlna_dms/ @chishm
|
||||||
/homeassistant/components/dnsip/ @gjohansson-ST
|
/homeassistant/components/dnsip/ @gjohansson-ST
|
||||||
/tests/components/dnsip/ @gjohansson-ST
|
/tests/components/dnsip/ @gjohansson-ST
|
||||||
|
/homeassistant/components/door/ @home-assistant/core
|
||||||
|
/tests/components/door/ @home-assistant/core
|
||||||
/homeassistant/components/doorbird/ @oblogic7 @bdraco @flacjacket
|
/homeassistant/components/doorbird/ @oblogic7 @bdraco @flacjacket
|
||||||
/tests/components/doorbird/ @oblogic7 @bdraco @flacjacket
|
/tests/components/doorbird/ @oblogic7 @bdraco @flacjacket
|
||||||
/homeassistant/components/dormakaba_dkey/ @emontnemery
|
/homeassistant/components/dormakaba_dkey/ @emontnemery
|
||||||
@@ -549,6 +557,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/freebox/ @hacf-fr @Quentame
|
/tests/components/freebox/ @hacf-fr @Quentame
|
||||||
/homeassistant/components/freedompro/ @stefano055415
|
/homeassistant/components/freedompro/ @stefano055415
|
||||||
/tests/components/freedompro/ @stefano055415
|
/tests/components/freedompro/ @stefano055415
|
||||||
|
/homeassistant/components/freshr/ @SierraNL
|
||||||
|
/tests/components/freshr/ @SierraNL
|
||||||
/homeassistant/components/fressnapf_tracker/ @eifinger
|
/homeassistant/components/fressnapf_tracker/ @eifinger
|
||||||
/tests/components/fressnapf_tracker/ @eifinger
|
/tests/components/fressnapf_tracker/ @eifinger
|
||||||
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||||
@@ -567,10 +577,14 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/fully_kiosk/ @cgarwood
|
/tests/components/fully_kiosk/ @cgarwood
|
||||||
/homeassistant/components/fyta/ @dontinelli
|
/homeassistant/components/fyta/ @dontinelli
|
||||||
/tests/components/fyta/ @dontinelli
|
/tests/components/fyta/ @dontinelli
|
||||||
|
/homeassistant/components/garage_door/ @home-assistant/core
|
||||||
|
/tests/components/garage_door/ @home-assistant/core
|
||||||
/homeassistant/components/garages_amsterdam/ @klaasnicolaas
|
/homeassistant/components/garages_amsterdam/ @klaasnicolaas
|
||||||
/tests/components/garages_amsterdam/ @klaasnicolaas
|
/tests/components/garages_amsterdam/ @klaasnicolaas
|
||||||
/homeassistant/components/gardena_bluetooth/ @elupus
|
/homeassistant/components/gardena_bluetooth/ @elupus
|
||||||
/tests/components/gardena_bluetooth/ @elupus
|
/tests/components/gardena_bluetooth/ @elupus
|
||||||
|
/homeassistant/components/gate/ @home-assistant/core
|
||||||
|
/tests/components/gate/ @home-assistant/core
|
||||||
/homeassistant/components/gdacs/ @exxamalte
|
/homeassistant/components/gdacs/ @exxamalte
|
||||||
/tests/components/gdacs/ @exxamalte
|
/tests/components/gdacs/ @exxamalte
|
||||||
/homeassistant/components/generic/ @davet2001
|
/homeassistant/components/generic/ @davet2001
|
||||||
@@ -723,8 +737,10 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/homewizard/ @DCSBL
|
/tests/components/homewizard/ @DCSBL
|
||||||
/homeassistant/components/honeywell/ @rdfurman @mkmer
|
/homeassistant/components/honeywell/ @rdfurman @mkmer
|
||||||
/tests/components/honeywell/ @rdfurman @mkmer
|
/tests/components/honeywell/ @rdfurman @mkmer
|
||||||
/homeassistant/components/html5/ @alexyao2015
|
/homeassistant/components/hr_energy_qube/ @MattieGit
|
||||||
/tests/components/html5/ @alexyao2015
|
/tests/components/hr_energy_qube/ @MattieGit
|
||||||
|
/homeassistant/components/html5/ @alexyao2015 @tr4nt0r
|
||||||
|
/tests/components/html5/ @alexyao2015 @tr4nt0r
|
||||||
/homeassistant/components/http/ @home-assistant/core
|
/homeassistant/components/http/ @home-assistant/core
|
||||||
/tests/components/http/ @home-assistant/core
|
/tests/components/http/ @home-assistant/core
|
||||||
/homeassistant/components/huawei_lte/ @scop @fphammerle
|
/homeassistant/components/huawei_lte/ @scop @fphammerle
|
||||||
@@ -737,6 +753,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/huisbaasje/ @dennisschroer
|
/tests/components/huisbaasje/ @dennisschroer
|
||||||
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
|
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
|
||||||
/tests/components/humidifier/ @home-assistant/core @Shulyaka
|
/tests/components/humidifier/ @home-assistant/core @Shulyaka
|
||||||
|
/homeassistant/components/humidity/ @home-assistant/core
|
||||||
|
/tests/components/humidity/ @home-assistant/core
|
||||||
/homeassistant/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
/homeassistant/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
||||||
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
||||||
/homeassistant/components/husqvarna_automower/ @Thomas55555
|
/homeassistant/components/husqvarna_automower/ @Thomas55555
|
||||||
@@ -768,6 +786,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/igloohome/ @keithle888
|
/tests/components/igloohome/ @keithle888
|
||||||
/homeassistant/components/ign_sismologia/ @exxamalte
|
/homeassistant/components/ign_sismologia/ @exxamalte
|
||||||
/tests/components/ign_sismologia/ @exxamalte
|
/tests/components/ign_sismologia/ @exxamalte
|
||||||
|
/homeassistant/components/illuminance/ @home-assistant/core
|
||||||
|
/tests/components/illuminance/ @home-assistant/core
|
||||||
/homeassistant/components/image/ @home-assistant/core
|
/homeassistant/components/image/ @home-assistant/core
|
||||||
/tests/components/image/ @home-assistant/core
|
/tests/components/image/ @home-assistant/core
|
||||||
/homeassistant/components/image_processing/ @home-assistant/core
|
/homeassistant/components/image_processing/ @home-assistant/core
|
||||||
@@ -786,8 +806,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/improv_ble/ @emontnemery
|
/tests/components/improv_ble/ @emontnemery
|
||||||
/homeassistant/components/incomfort/ @jbouwh
|
/homeassistant/components/incomfort/ @jbouwh
|
||||||
/tests/components/incomfort/ @jbouwh
|
/tests/components/incomfort/ @jbouwh
|
||||||
/homeassistant/components/indevolt/ @xirtnl
|
/homeassistant/components/indevolt/ @xirt
|
||||||
/tests/components/indevolt/ @xirtnl
|
/tests/components/indevolt/ @xirt
|
||||||
/homeassistant/components/inels/ @epdevlab
|
/homeassistant/components/inels/ @epdevlab
|
||||||
/tests/components/inels/ @epdevlab
|
/tests/components/inels/ @epdevlab
|
||||||
/homeassistant/components/influxdb/ @mdegat01 @Robbie1221
|
/homeassistant/components/influxdb/ @mdegat01 @Robbie1221
|
||||||
@@ -927,12 +947,16 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/lektrico/ @lektrico
|
/tests/components/lektrico/ @lektrico
|
||||||
/homeassistant/components/letpot/ @jpelgrom
|
/homeassistant/components/letpot/ @jpelgrom
|
||||||
/tests/components/letpot/ @jpelgrom
|
/tests/components/letpot/ @jpelgrom
|
||||||
|
/homeassistant/components/lg_infrared/ @home-assistant/core
|
||||||
|
/tests/components/lg_infrared/ @home-assistant/core
|
||||||
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
||||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||||
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
||||||
/tests/components/lg_thinq/ @LG-ThinQ-Integration
|
/tests/components/lg_thinq/ @LG-ThinQ-Integration
|
||||||
/homeassistant/components/libre_hardware_monitor/ @Sab44
|
/homeassistant/components/libre_hardware_monitor/ @Sab44
|
||||||
/tests/components/libre_hardware_monitor/ @Sab44
|
/tests/components/libre_hardware_monitor/ @Sab44
|
||||||
|
/homeassistant/components/lichess/ @aryanhasgithub
|
||||||
|
/tests/components/lichess/ @aryanhasgithub
|
||||||
/homeassistant/components/lidarr/ @tkdrob
|
/homeassistant/components/lidarr/ @tkdrob
|
||||||
/tests/components/lidarr/ @tkdrob
|
/tests/components/lidarr/ @tkdrob
|
||||||
/homeassistant/components/liebherr/ @mettolen
|
/homeassistant/components/liebherr/ @mettolen
|
||||||
@@ -962,6 +986,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/logbook/ @home-assistant/core
|
/tests/components/logbook/ @home-assistant/core
|
||||||
/homeassistant/components/logger/ @home-assistant/core
|
/homeassistant/components/logger/ @home-assistant/core
|
||||||
/tests/components/logger/ @home-assistant/core
|
/tests/components/logger/ @home-assistant/core
|
||||||
|
/homeassistant/components/lojack/ @devinslick
|
||||||
|
/tests/components/lojack/ @devinslick
|
||||||
/homeassistant/components/london_underground/ @jpbede
|
/homeassistant/components/london_underground/ @jpbede
|
||||||
/tests/components/london_underground/ @jpbede
|
/tests/components/london_underground/ @jpbede
|
||||||
/homeassistant/components/lookin/ @ANMalko @bdraco
|
/homeassistant/components/lookin/ @ANMalko @bdraco
|
||||||
@@ -1051,6 +1077,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/modern_forms/ @wonderslug
|
/tests/components/modern_forms/ @wonderslug
|
||||||
/homeassistant/components/moehlenhoff_alpha2/ @j-a-n
|
/homeassistant/components/moehlenhoff_alpha2/ @j-a-n
|
||||||
/tests/components/moehlenhoff_alpha2/ @j-a-n
|
/tests/components/moehlenhoff_alpha2/ @j-a-n
|
||||||
|
/homeassistant/components/moisture/ @home-assistant/core
|
||||||
|
/tests/components/moisture/ @home-assistant/core
|
||||||
/homeassistant/components/monarch_money/ @jeeftor
|
/homeassistant/components/monarch_money/ @jeeftor
|
||||||
/tests/components/monarch_money/ @jeeftor
|
/tests/components/monarch_money/ @jeeftor
|
||||||
/homeassistant/components/monoprice/ @etsinko @OnFreund
|
/homeassistant/components/monoprice/ @etsinko @OnFreund
|
||||||
@@ -1061,6 +1089,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/moon/ @fabaff @frenck
|
/tests/components/moon/ @fabaff @frenck
|
||||||
/homeassistant/components/mopeka/ @bdraco
|
/homeassistant/components/mopeka/ @bdraco
|
||||||
/tests/components/mopeka/ @bdraco
|
/tests/components/mopeka/ @bdraco
|
||||||
|
/homeassistant/components/motion/ @home-assistant/core
|
||||||
|
/tests/components/motion/ @home-assistant/core
|
||||||
/homeassistant/components/motion_blinds/ @starkillerOG
|
/homeassistant/components/motion_blinds/ @starkillerOG
|
||||||
/tests/components/motion_blinds/ @starkillerOG
|
/tests/components/motion_blinds/ @starkillerOG
|
||||||
/homeassistant/components/motionblinds_ble/ @LennP @jerrybboy
|
/homeassistant/components/motionblinds_ble/ @LennP @jerrybboy
|
||||||
@@ -1174,6 +1204,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/nzbget/ @chriscla
|
/tests/components/nzbget/ @chriscla
|
||||||
/homeassistant/components/obihai/ @dshokouhi @ejpenney
|
/homeassistant/components/obihai/ @dshokouhi @ejpenney
|
||||||
/tests/components/obihai/ @dshokouhi @ejpenney
|
/tests/components/obihai/ @dshokouhi @ejpenney
|
||||||
|
/homeassistant/components/occupancy/ @home-assistant/core
|
||||||
|
/tests/components/occupancy/ @home-assistant/core
|
||||||
/homeassistant/components/octoprint/ @rfleming71
|
/homeassistant/components/octoprint/ @rfleming71
|
||||||
/tests/components/octoprint/ @rfleming71
|
/tests/components/octoprint/ @rfleming71
|
||||||
/homeassistant/components/ohmconnect/ @robbiet480
|
/homeassistant/components/ohmconnect/ @robbiet480
|
||||||
@@ -1194,12 +1226,14 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/onewire/ @garbled1 @epenet
|
/tests/components/onewire/ @garbled1 @epenet
|
||||||
/homeassistant/components/onkyo/ @arturpragacz @eclair4151
|
/homeassistant/components/onkyo/ @arturpragacz @eclair4151
|
||||||
/tests/components/onkyo/ @arturpragacz @eclair4151
|
/tests/components/onkyo/ @arturpragacz @eclair4151
|
||||||
/homeassistant/components/onvif/ @hunterjm @jterrace
|
/homeassistant/components/onvif/ @jterrace
|
||||||
/tests/components/onvif/ @hunterjm @jterrace
|
/tests/components/onvif/ @jterrace
|
||||||
/homeassistant/components/open_meteo/ @frenck
|
/homeassistant/components/open_meteo/ @frenck
|
||||||
/tests/components/open_meteo/ @frenck
|
/tests/components/open_meteo/ @frenck
|
||||||
/homeassistant/components/open_router/ @joostlek
|
/homeassistant/components/open_router/ @joostlek @ab3lson
|
||||||
/tests/components/open_router/ @joostlek
|
/tests/components/open_router/ @joostlek @ab3lson
|
||||||
|
/homeassistant/components/opendisplay/ @g4bri3lDev
|
||||||
|
/tests/components/opendisplay/ @g4bri3lDev
|
||||||
/homeassistant/components/openerz/ @misialq
|
/homeassistant/components/openerz/ @misialq
|
||||||
/tests/components/openerz/ @misialq
|
/tests/components/openerz/ @misialq
|
||||||
/homeassistant/components/openevse/ @c00w @firstof9
|
/homeassistant/components/openevse/ @c00w @firstof9
|
||||||
@@ -1283,6 +1317,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/poolsense/ @haemishkyd
|
/tests/components/poolsense/ @haemishkyd
|
||||||
/homeassistant/components/portainer/ @erwindouna
|
/homeassistant/components/portainer/ @erwindouna
|
||||||
/tests/components/portainer/ @erwindouna
|
/tests/components/portainer/ @erwindouna
|
||||||
|
/homeassistant/components/power/ @home-assistant/core
|
||||||
|
/tests/components/power/ @home-assistant/core
|
||||||
/homeassistant/components/powerfox/ @klaasnicolaas
|
/homeassistant/components/powerfox/ @klaasnicolaas
|
||||||
/tests/components/powerfox/ @klaasnicolaas
|
/tests/components/powerfox/ @klaasnicolaas
|
||||||
/homeassistant/components/powerfox_local/ @klaasnicolaas
|
/homeassistant/components/powerfox_local/ @klaasnicolaas
|
||||||
@@ -1305,8 +1341,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/prosegur/ @dgomes
|
/tests/components/prosegur/ @dgomes
|
||||||
/homeassistant/components/proximity/ @mib1185
|
/homeassistant/components/proximity/ @mib1185
|
||||||
/tests/components/proximity/ @mib1185
|
/tests/components/proximity/ @mib1185
|
||||||
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno @erwindouna
|
/homeassistant/components/proxmoxve/ @Corbeno @erwindouna @CoMPaTech
|
||||||
/tests/components/proxmoxve/ @jhollowe @Corbeno @erwindouna
|
/tests/components/proxmoxve/ @Corbeno @erwindouna @CoMPaTech
|
||||||
/homeassistant/components/ps4/ @ktnrg45
|
/homeassistant/components/ps4/ @ktnrg45
|
||||||
/tests/components/ps4/ @ktnrg45
|
/tests/components/ps4/ @ktnrg45
|
||||||
/homeassistant/components/pterodactyl/ @elmurato
|
/homeassistant/components/pterodactyl/ @elmurato
|
||||||
@@ -1541,8 +1577,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/sma/ @kellerza @rklomp @erwindouna
|
/tests/components/sma/ @kellerza @rklomp @erwindouna
|
||||||
/homeassistant/components/smappee/ @bsmappee
|
/homeassistant/components/smappee/ @bsmappee
|
||||||
/tests/components/smappee/ @bsmappee
|
/tests/components/smappee/ @bsmappee
|
||||||
/homeassistant/components/smarla/ @explicatis @rlint-explicatis
|
/homeassistant/components/smarla/ @explicatis @johannes-exp
|
||||||
/tests/components/smarla/ @explicatis @rlint-explicatis
|
/tests/components/smarla/ @explicatis @johannes-exp
|
||||||
/homeassistant/components/smart_meter_texas/ @grahamwetzler
|
/homeassistant/components/smart_meter_texas/ @grahamwetzler
|
||||||
/tests/components/smart_meter_texas/ @grahamwetzler
|
/tests/components/smart_meter_texas/ @grahamwetzler
|
||||||
/homeassistant/components/smartthings/ @joostlek
|
/homeassistant/components/smartthings/ @joostlek
|
||||||
@@ -1568,6 +1604,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/solaredge_local/ @drobtravels @scheric
|
/homeassistant/components/solaredge_local/ @drobtravels @scheric
|
||||||
/homeassistant/components/solarlog/ @Ernst79 @dontinelli
|
/homeassistant/components/solarlog/ @Ernst79 @dontinelli
|
||||||
/tests/components/solarlog/ @Ernst79 @dontinelli
|
/tests/components/solarlog/ @Ernst79 @dontinelli
|
||||||
|
/homeassistant/components/solarman/ @solarmanpv
|
||||||
|
/tests/components/solarman/ @solarmanpv
|
||||||
/homeassistant/components/solax/ @squishykid @Darsstar
|
/homeassistant/components/solax/ @squishykid @Darsstar
|
||||||
/tests/components/solax/ @squishykid @Darsstar
|
/tests/components/solax/ @squishykid @Darsstar
|
||||||
/homeassistant/components/soma/ @ratsept
|
/homeassistant/components/soma/ @ratsept
|
||||||
@@ -1596,8 +1634,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/srp_energy/ @briglx
|
/tests/components/srp_energy/ @briglx
|
||||||
/homeassistant/components/starline/ @anonym-tsk
|
/homeassistant/components/starline/ @anonym-tsk
|
||||||
/tests/components/starline/ @anonym-tsk
|
/tests/components/starline/ @anonym-tsk
|
||||||
/homeassistant/components/starlink/ @boswelja
|
|
||||||
/tests/components/starlink/ @boswelja
|
|
||||||
/homeassistant/components/statistics/ @ThomDietrich @gjohansson-ST
|
/homeassistant/components/statistics/ @ThomDietrich @gjohansson-ST
|
||||||
/tests/components/statistics/ @ThomDietrich @gjohansson-ST
|
/tests/components/statistics/ @ThomDietrich @gjohansson-ST
|
||||||
/homeassistant/components/steam_online/ @tkdrob
|
/homeassistant/components/steam_online/ @tkdrob
|
||||||
@@ -1650,8 +1686,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/system_bridge/ @timmo001
|
/tests/components/system_bridge/ @timmo001
|
||||||
/homeassistant/components/systemmonitor/ @gjohansson-ST
|
/homeassistant/components/systemmonitor/ @gjohansson-ST
|
||||||
/tests/components/systemmonitor/ @gjohansson-ST
|
/tests/components/systemmonitor/ @gjohansson-ST
|
||||||
/homeassistant/components/systemnexa2/ @konsulten @slangstrom
|
/homeassistant/components/systemnexa2/ @konsulten
|
||||||
/tests/components/systemnexa2/ @konsulten @slangstrom
|
/tests/components/systemnexa2/ @konsulten
|
||||||
/homeassistant/components/tado/ @erwindouna
|
/homeassistant/components/tado/ @erwindouna
|
||||||
/tests/components/tado/ @erwindouna
|
/tests/components/tado/ @erwindouna
|
||||||
/homeassistant/components/tag/ @home-assistant/core
|
/homeassistant/components/tag/ @home-assistant/core
|
||||||
@@ -1679,6 +1715,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/tellduslive/ @fredrike
|
/tests/components/tellduslive/ @fredrike
|
||||||
/homeassistant/components/teltonika/ @karlbeecken
|
/homeassistant/components/teltonika/ @karlbeecken
|
||||||
/tests/components/teltonika/ @karlbeecken
|
/tests/components/teltonika/ @karlbeecken
|
||||||
|
/homeassistant/components/temperature/ @home-assistant/core
|
||||||
|
/tests/components/temperature/ @home-assistant/core
|
||||||
/homeassistant/components/template/ @Petro31 @home-assistant/core
|
/homeassistant/components/template/ @Petro31 @home-assistant/core
|
||||||
/tests/components/template/ @Petro31 @home-assistant/core
|
/tests/components/template/ @Petro31 @home-assistant/core
|
||||||
/homeassistant/components/tesla_fleet/ @Bre77
|
/homeassistant/components/tesla_fleet/ @Bre77
|
||||||
@@ -1691,7 +1729,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/tessie/ @Bre77
|
/tests/components/tessie/ @Bre77
|
||||||
/homeassistant/components/text/ @home-assistant/core
|
/homeassistant/components/text/ @home-assistant/core
|
||||||
/tests/components/text/ @home-assistant/core
|
/tests/components/text/ @home-assistant/core
|
||||||
/homeassistant/components/tfiac/ @fredrike @mellado
|
|
||||||
/homeassistant/components/thermobeacon/ @bdraco
|
/homeassistant/components/thermobeacon/ @bdraco
|
||||||
/tests/components/thermobeacon/ @bdraco
|
/tests/components/thermobeacon/ @bdraco
|
||||||
/homeassistant/components/thermopro/ @bdraco @h3ss
|
/homeassistant/components/thermopro/ @bdraco @h3ss
|
||||||
@@ -1725,6 +1762,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/tomorrowio/ @raman325 @lymanepp
|
/tests/components/tomorrowio/ @raman325 @lymanepp
|
||||||
/homeassistant/components/totalconnect/ @austinmroczek
|
/homeassistant/components/totalconnect/ @austinmroczek
|
||||||
/tests/components/totalconnect/ @austinmroczek
|
/tests/components/totalconnect/ @austinmroczek
|
||||||
|
/homeassistant/components/touchline/ @mnordseth
|
||||||
|
/tests/components/touchline/ @mnordseth
|
||||||
/homeassistant/components/touchline_sl/ @jnsgruk
|
/homeassistant/components/touchline_sl/ @jnsgruk
|
||||||
/tests/components/touchline_sl/ @jnsgruk
|
/tests/components/touchline_sl/ @jnsgruk
|
||||||
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
|
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
|
||||||
@@ -1753,6 +1792,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/trend/ @jpbede
|
/tests/components/trend/ @jpbede
|
||||||
/homeassistant/components/triggercmd/ @rvmey
|
/homeassistant/components/triggercmd/ @rvmey
|
||||||
/tests/components/triggercmd/ @rvmey
|
/tests/components/triggercmd/ @rvmey
|
||||||
|
/homeassistant/components/trmnl/ @joostlek
|
||||||
|
/tests/components/trmnl/ @joostlek
|
||||||
/homeassistant/components/tts/ @home-assistant/core
|
/homeassistant/components/tts/ @home-assistant/core
|
||||||
/tests/components/tts/ @home-assistant/core
|
/tests/components/tts/ @home-assistant/core
|
||||||
/homeassistant/components/tuya/ @Tuya @zlinoliver
|
/homeassistant/components/tuya/ @Tuya @zlinoliver
|
||||||
@@ -1769,6 +1810,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ukraine_alarm/ @PaulAnnekov
|
/tests/components/ukraine_alarm/ @PaulAnnekov
|
||||||
/homeassistant/components/unifi/ @Kane610
|
/homeassistant/components/unifi/ @Kane610
|
||||||
/tests/components/unifi/ @Kane610
|
/tests/components/unifi/ @Kane610
|
||||||
|
/homeassistant/components/unifi_access/ @imhotep @RaHehl
|
||||||
|
/tests/components/unifi_access/ @imhotep @RaHehl
|
||||||
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
|
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
|
||||||
/homeassistant/components/unifiled/ @florisvdk
|
/homeassistant/components/unifiled/ @florisvdk
|
||||||
/homeassistant/components/unifiprotect/ @RaHehl
|
/homeassistant/components/unifiprotect/ @RaHehl
|
||||||
@@ -1808,8 +1851,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/vegehub/ @thulrus
|
/tests/components/vegehub/ @thulrus
|
||||||
/homeassistant/components/velbus/ @Cereal2nd @brefra
|
/homeassistant/components/velbus/ @Cereal2nd @brefra
|
||||||
/tests/components/velbus/ @Cereal2nd @brefra
|
/tests/components/velbus/ @Cereal2nd @brefra
|
||||||
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
|
/homeassistant/components/velux/ @Julius2342 @pawlizio @wollew
|
||||||
/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
|
/tests/components/velux/ @Julius2342 @pawlizio @wollew
|
||||||
/homeassistant/components/venstar/ @garbled1 @jhollowe
|
/homeassistant/components/venstar/ @garbled1 @jhollowe
|
||||||
/tests/components/venstar/ @garbled1 @jhollowe
|
/tests/components/venstar/ @garbled1 @jhollowe
|
||||||
/homeassistant/components/versasense/ @imstevenxyz
|
/homeassistant/components/versasense/ @imstevenxyz
|
||||||
@@ -1892,8 +1935,12 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/whois/ @frenck
|
/tests/components/whois/ @frenck
|
||||||
/homeassistant/components/wiffi/ @mampfes
|
/homeassistant/components/wiffi/ @mampfes
|
||||||
/tests/components/wiffi/ @mampfes
|
/tests/components/wiffi/ @mampfes
|
||||||
|
/homeassistant/components/wiim/ @Linkplay2020
|
||||||
|
/tests/components/wiim/ @Linkplay2020
|
||||||
/homeassistant/components/wilight/ @leofig-rj
|
/homeassistant/components/wilight/ @leofig-rj
|
||||||
/tests/components/wilight/ @leofig-rj
|
/tests/components/wilight/ @leofig-rj
|
||||||
|
/homeassistant/components/window/ @home-assistant/core
|
||||||
|
/tests/components/window/ @home-assistant/core
|
||||||
/homeassistant/components/wirelesstag/ @sergeymaysak
|
/homeassistant/components/wirelesstag/ @sergeymaysak
|
||||||
/homeassistant/components/withings/ @joostlek
|
/homeassistant/components/withings/ @joostlek
|
||||||
/tests/components/withings/ @joostlek
|
/tests/components/withings/ @joostlek
|
||||||
|
|||||||
Generated
+1
-2
@@ -10,7 +10,6 @@ LABEL \
|
|||||||
org.opencontainers.image.description="Open-source home automation platform running on Python 3" \
|
org.opencontainers.image.description="Open-source home automation platform running on Python 3" \
|
||||||
org.opencontainers.image.documentation="https://www.home-assistant.io/docs/" \
|
org.opencontainers.image.documentation="https://www.home-assistant.io/docs/" \
|
||||||
org.opencontainers.image.licenses="Apache-2.0" \
|
org.opencontainers.image.licenses="Apache-2.0" \
|
||||||
org.opencontainers.image.source="https://github.com/home-assistant/core" \
|
|
||||||
org.opencontainers.image.title="Home Assistant" \
|
org.opencontainers.image.title="Home Assistant" \
|
||||||
org.opencontainers.image.url="https://www.home-assistant.io/"
|
org.opencontainers.image.url="https://www.home-assistant.io/"
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ RUN \
|
|||||||
# Verify go2rtc can be executed
|
# Verify go2rtc can be executed
|
||||||
go2rtc --version \
|
go2rtc --version \
|
||||||
# Install uv
|
# Install uv
|
||||||
&& pip3 install uv==0.10.6
|
&& pip3 install uv==0.11.1
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
|||||||
+71
-22
@@ -70,7 +70,7 @@ from .const import (
|
|||||||
SIGNAL_BOOTSTRAP_INTEGRATIONS,
|
SIGNAL_BOOTSTRAP_INTEGRATIONS,
|
||||||
)
|
)
|
||||||
from .core_config import async_process_ha_core_config
|
from .core_config import async_process_ha_core_config
|
||||||
from .exceptions import HomeAssistantError
|
from .exceptions import HomeAssistantError, UnsupportedStorageVersionError
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
area_registry,
|
area_registry,
|
||||||
category_registry,
|
category_registry,
|
||||||
@@ -236,9 +236,31 @@ DEFAULT_INTEGRATIONS = {
|
|||||||
"input_text",
|
"input_text",
|
||||||
"schedule",
|
"schedule",
|
||||||
"timer",
|
"timer",
|
||||||
|
#
|
||||||
|
# Base platforms:
|
||||||
|
# Note: Calendar and todo are not included to prevent them from registering
|
||||||
|
# their frontend panels when there are no calendar or todo integrations.
|
||||||
|
*(BASE_PLATFORMS - {"calendar", "todo"}),
|
||||||
|
#
|
||||||
|
# Integrations providing triggers and conditions for base platforms:
|
||||||
|
"air_quality",
|
||||||
|
"battery",
|
||||||
|
"door",
|
||||||
|
"garage_door",
|
||||||
|
"gate",
|
||||||
|
"humidity",
|
||||||
|
"illuminance",
|
||||||
|
"moisture",
|
||||||
|
"motion",
|
||||||
|
"occupancy",
|
||||||
|
"power",
|
||||||
|
"temperature",
|
||||||
|
"window",
|
||||||
}
|
}
|
||||||
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
|
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
|
||||||
# These integrations are set up if recovery mode is activated.
|
# These integrations are set up if recovery mode is activated.
|
||||||
|
"backup",
|
||||||
|
"cloud",
|
||||||
"frontend",
|
"frontend",
|
||||||
}
|
}
|
||||||
DEFAULT_INTEGRATIONS_SUPERVISOR = {
|
DEFAULT_INTEGRATIONS_SUPERVISOR = {
|
||||||
@@ -433,32 +455,57 @@ def _init_blocking_io_modules_in_executor() -> None:
|
|||||||
is_docker_env()
|
is_docker_env()
|
||||||
|
|
||||||
|
|
||||||
async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
async def async_load_base_functionality(hass: core.HomeAssistant) -> bool:
|
||||||
"""Load the registries and modules that will do blocking I/O."""
|
"""Load the registries and modules that will do blocking I/O.
|
||||||
|
|
||||||
|
Return whether loading succeeded.
|
||||||
|
"""
|
||||||
if DATA_REGISTRIES_LOADED in hass.data:
|
if DATA_REGISTRIES_LOADED in hass.data:
|
||||||
return
|
return True
|
||||||
|
|
||||||
hass.data[DATA_REGISTRIES_LOADED] = None
|
hass.data[DATA_REGISTRIES_LOADED] = None
|
||||||
entity.async_setup(hass)
|
entity.async_setup(hass)
|
||||||
frame.async_setup(hass)
|
frame.async_setup(hass)
|
||||||
template.async_setup(hass)
|
template.async_setup(hass)
|
||||||
translation.async_setup(hass)
|
translation.async_setup(hass)
|
||||||
await asyncio.gather(
|
|
||||||
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
recovery = hass.config.recovery_mode
|
||||||
create_eager_task(area_registry.async_load(hass)),
|
device_registry.async_setup(hass)
|
||||||
create_eager_task(category_registry.async_load(hass)),
|
try:
|
||||||
create_eager_task(device_registry.async_load(hass)),
|
await asyncio.gather(
|
||||||
create_eager_task(entity_registry.async_load(hass)),
|
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
||||||
create_eager_task(floor_registry.async_load(hass)),
|
create_eager_task(area_registry.async_load(hass, load_empty=recovery)),
|
||||||
create_eager_task(issue_registry.async_load(hass)),
|
create_eager_task(category_registry.async_load(hass, load_empty=recovery)),
|
||||||
create_eager_task(label_registry.async_load(hass)),
|
create_eager_task(device_registry.async_load(hass, load_empty=recovery)),
|
||||||
hass.async_add_executor_job(_init_blocking_io_modules_in_executor),
|
create_eager_task(entity_registry.async_load(hass, load_empty=recovery)),
|
||||||
create_eager_task(template.async_load_custom_templates(hass)),
|
create_eager_task(floor_registry.async_load(hass, load_empty=recovery)),
|
||||||
create_eager_task(restore_state.async_load(hass)),
|
create_eager_task(issue_registry.async_load(hass, load_empty=recovery)),
|
||||||
create_eager_task(hass.config_entries.async_initialize()),
|
create_eager_task(label_registry.async_load(hass, load_empty=recovery)),
|
||||||
create_eager_task(async_get_system_info(hass)),
|
hass.async_add_executor_job(_init_blocking_io_modules_in_executor),
|
||||||
create_eager_task(condition.async_setup(hass)),
|
create_eager_task(template.async_load_custom_templates(hass)),
|
||||||
create_eager_task(trigger.async_setup(hass)),
|
create_eager_task(restore_state.async_load(hass, load_empty=recovery)),
|
||||||
)
|
create_eager_task(hass.config_entries.async_initialize()),
|
||||||
|
create_eager_task(async_get_system_info(hass)),
|
||||||
|
create_eager_task(condition.async_setup(hass)),
|
||||||
|
create_eager_task(trigger.async_setup(hass)),
|
||||||
|
)
|
||||||
|
except UnsupportedStorageVersionError as err:
|
||||||
|
# If we're already in recovery mode, we don't want to handle the exception
|
||||||
|
# and activate recovery mode again, as that would lead to an infinite loop.
|
||||||
|
if recovery:
|
||||||
|
raise
|
||||||
|
|
||||||
|
_LOGGER.error(
|
||||||
|
"Storage file %s was created by a newer version of Home Assistant"
|
||||||
|
" (storage version %s > %s); activating recovery mode; on-disk data"
|
||||||
|
" is preserved; upgrade Home Assistant or restore from a backup",
|
||||||
|
err.storage_key,
|
||||||
|
err.found_version,
|
||||||
|
err.max_supported_version,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_from_config_dict(
|
async def async_from_config_dict(
|
||||||
@@ -475,7 +522,9 @@ async def async_from_config_dict(
|
|||||||
# Prime custom component cache early so we know if registry entries are tied
|
# Prime custom component cache early so we know if registry entries are tied
|
||||||
# to a custom integration
|
# to a custom integration
|
||||||
await loader.async_get_custom_components(hass)
|
await loader.async_get_custom_components(hass)
|
||||||
await async_load_base_functionality(hass)
|
|
||||||
|
if not await async_load_base_functionality(hass):
|
||||||
|
return None
|
||||||
|
|
||||||
# Set up core.
|
# Set up core.
|
||||||
_LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)
|
_LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
{
|
{
|
||||||
"domain": "lg",
|
"domain": "lg",
|
||||||
"name": "LG",
|
"name": "LG",
|
||||||
"integrations": ["lg_netcast", "lg_soundbar", "lg_thinq", "webostv"]
|
"integrations": [
|
||||||
|
"lg_infrared",
|
||||||
|
"lg_netcast",
|
||||||
|
"lg_soundbar",
|
||||||
|
"lg_thinq",
|
||||||
|
"webostv"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
{
|
{
|
||||||
"domain": "ubiquiti",
|
"domain": "ubiquiti",
|
||||||
"name": "Ubiquiti",
|
"name": "Ubiquiti",
|
||||||
"integrations": ["airos", "unifi", "unifi_direct", "unifiled", "unifiprotect"]
|
"integrations": [
|
||||||
|
"airos",
|
||||||
|
"unifi",
|
||||||
|
"unifi_access",
|
||||||
|
"unifi_direct",
|
||||||
|
"unifiled",
|
||||||
|
"unifiprotect"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "ubisys",
|
||||||
|
"name": "Ubisys",
|
||||||
|
"iot_standards": ["zigbee"]
|
||||||
|
}
|
||||||
@@ -9,6 +9,6 @@
|
|||||||
},
|
},
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["jaraco.abode", "lomond"],
|
"loggers": ["jaraco.abode", "lomond"],
|
||||||
"requirements": ["jaraco.abode==6.2.1"],
|
"requirements": ["jaraco.abode==6.4.0"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
|
|||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
|
from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
|
||||||
@@ -40,6 +41,7 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
|
|||||||
AbodeSensorDescription(
|
AbodeSensorDescription(
|
||||||
key="temperature",
|
key="temperature",
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[
|
native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[
|
||||||
device.temp_unit
|
device.temp_unit
|
||||||
],
|
],
|
||||||
@@ -48,12 +50,14 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
|
|||||||
AbodeSensorDescription(
|
AbodeSensorDescription(
|
||||||
key="humidity",
|
key="humidity",
|
||||||
device_class=SensorDeviceClass.HUMIDITY,
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement_fn=lambda _: PERCENTAGE,
|
native_unit_of_measurement_fn=lambda _: PERCENTAGE,
|
||||||
value_fn=lambda device: cast(float, device.humidity),
|
value_fn=lambda device: cast(float, device.humidity),
|
||||||
),
|
),
|
||||||
AbodeSensorDescription(
|
AbodeSensorDescription(
|
||||||
key="lux",
|
key="lux",
|
||||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement_fn=lambda _: LIGHT_LUX,
|
native_unit_of_measurement_fn=lambda _: LIGHT_LUX,
|
||||||
value_fn=lambda device: cast(float, device.lux),
|
value_fn=lambda device: cast(float, device.lux),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
"""The actiontec component."""
|
"""The Actiontec integration."""
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="timeout",
|
step_id="timeout",
|
||||||
)
|
)
|
||||||
del self.login_task
|
self.login_task = None
|
||||||
return await self.async_step_user()
|
return await self.async_step_user()
|
||||||
|
|
||||||
async def async_step_reauth(
|
async def async_step_reauth(
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/actron_air",
|
"documentation": "https://www.home-assistant.io/integrations/actron_air",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "silver",
|
||||||
"requirements": ["actron-neo-api==0.4.1"]
|
"requirements": ["actron-neo-api==0.4.1"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ rules:
|
|||||||
log-when-unavailable: done
|
log-when-unavailable: done
|
||||||
parallel-updates: done
|
parallel-updates: done
|
||||||
reauthentication-flow: done
|
reauthentication-flow: done
|
||||||
test-coverage: todo
|
test-coverage: done
|
||||||
|
|
||||||
# Gold
|
# Gold
|
||||||
devices: done
|
devices: done
|
||||||
|
|||||||
Regular → Executable
+34
-6
@@ -168,29 +168,57 @@ class LocalAdaxDevice(CoordinatorEntity[AdaxLocalCoordinator], ClimateEntity):
|
|||||||
if hvac_mode == HVACMode.HEAT:
|
if hvac_mode == HVACMode.HEAT:
|
||||||
temperature = self._attr_target_temperature or self._attr_min_temp
|
temperature = self._attr_target_temperature or self._attr_min_temp
|
||||||
await self._adax_data_handler.set_target_temperature(temperature)
|
await self._adax_data_handler.set_target_temperature(temperature)
|
||||||
|
self._attr_target_temperature = temperature
|
||||||
|
self._attr_icon = "mdi:radiator"
|
||||||
elif hvac_mode == HVACMode.OFF:
|
elif hvac_mode == HVACMode.OFF:
|
||||||
await self._adax_data_handler.set_target_temperature(0)
|
await self._adax_data_handler.set_target_temperature(0)
|
||||||
|
self._attr_icon = "mdi:radiator-off"
|
||||||
|
else:
|
||||||
|
# Ignore unsupported HVAC modes to avoid desynchronizing entity state
|
||||||
|
# from the physical device.
|
||||||
|
return
|
||||||
|
|
||||||
|
self._attr_hvac_mode = hvac_mode
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
||||||
return
|
return
|
||||||
await self._adax_data_handler.set_target_temperature(temperature)
|
if self._attr_hvac_mode == HVACMode.HEAT:
|
||||||
|
await self._adax_data_handler.set_target_temperature(temperature)
|
||||||
|
|
||||||
@callback
|
self._attr_target_temperature = temperature
|
||||||
def _handle_coordinator_update(self) -> None:
|
self.async_write_ha_state()
|
||||||
"""Handle updated data from the coordinator."""
|
|
||||||
|
def _update_hvac_attributes(self) -> None:
|
||||||
|
"""Update hvac mode and temperatures from coordinator data.
|
||||||
|
|
||||||
|
The coordinator reports a target temperature of 0 when the heater is
|
||||||
|
turned off. In that case, only the hvac mode and icon are updated and
|
||||||
|
the previous non-zero target temperature is preserved. When the
|
||||||
|
reported target temperature is non-zero, the stored target temperature
|
||||||
|
is updated to match the coordinator value.
|
||||||
|
"""
|
||||||
if data := self.coordinator.data:
|
if data := self.coordinator.data:
|
||||||
self._attr_current_temperature = data["current_temperature"]
|
self._attr_current_temperature = data["current_temperature"]
|
||||||
self._attr_available = self._attr_current_temperature is not None
|
|
||||||
if (target_temp := data["target_temperature"]) == 0:
|
if (target_temp := data["target_temperature"]) == 0:
|
||||||
self._attr_hvac_mode = HVACMode.OFF
|
self._attr_hvac_mode = HVACMode.OFF
|
||||||
self._attr_icon = "mdi:radiator-off"
|
self._attr_icon = "mdi:radiator-off"
|
||||||
if target_temp == 0:
|
if self._attr_target_temperature is None:
|
||||||
self._attr_target_temperature = self._attr_min_temp
|
self._attr_target_temperature = self._attr_min_temp
|
||||||
else:
|
else:
|
||||||
self._attr_hvac_mode = HVACMode.HEAT
|
self._attr_hvac_mode = HVACMode.HEAT
|
||||||
self._attr_icon = "mdi:radiator"
|
self._attr_icon = "mdi:radiator"
|
||||||
self._attr_target_temperature = target_temp
|
self._attr_target_temperature = target_temp
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
"""Handle updated data from the coordinator."""
|
||||||
|
self._update_hvac_attributes()
|
||||||
super()._handle_coordinator_update()
|
super()._handle_coordinator_update()
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""When entity is added to hass."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
self._update_hvac_attributes()
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""Provides conditions for air quality."""
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import (
|
||||||
|
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||||
|
BinarySensorDeviceClass,
|
||||||
|
)
|
||||||
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
CONCENTRATION_PARTS_PER_BILLION,
|
||||||
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_ON,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.automation import DomainSpec
|
||||||
|
from homeassistant.helpers.condition import (
|
||||||
|
Condition,
|
||||||
|
make_entity_numerical_condition,
|
||||||
|
make_entity_numerical_condition_with_unit,
|
||||||
|
make_entity_state_condition,
|
||||||
|
)
|
||||||
|
from homeassistant.util.unit_conversion import (
|
||||||
|
CarbonMonoxideConcentrationConverter,
|
||||||
|
MassVolumeConcentrationConverter,
|
||||||
|
NitrogenDioxideConcentrationConverter,
|
||||||
|
NitrogenMonoxideConcentrationConverter,
|
||||||
|
OzoneConcentrationConverter,
|
||||||
|
SulphurDioxideConcentrationConverter,
|
||||||
|
UnitlessRatioConverter,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_detected_condition(
|
||||||
|
device_class: BinarySensorDeviceClass,
|
||||||
|
) -> type[Condition]:
|
||||||
|
"""Create a detected condition for a binary sensor device class."""
|
||||||
|
return make_entity_state_condition(
|
||||||
|
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_ON
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_cleared_condition(
|
||||||
|
device_class: BinarySensorDeviceClass,
|
||||||
|
) -> type[Condition]:
|
||||||
|
"""Create a cleared condition for a binary sensor device class."""
|
||||||
|
return make_entity_state_condition(
|
||||||
|
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_OFF
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CONDITIONS: dict[str, type[Condition]] = {
|
||||||
|
# Binary sensor conditions (detected/cleared)
|
||||||
|
"is_gas_detected": _make_detected_condition(BinarySensorDeviceClass.GAS),
|
||||||
|
"is_gas_cleared": _make_cleared_condition(BinarySensorDeviceClass.GAS),
|
||||||
|
"is_co_detected": _make_detected_condition(BinarySensorDeviceClass.CO),
|
||||||
|
"is_co_cleared": _make_cleared_condition(BinarySensorDeviceClass.CO),
|
||||||
|
"is_smoke_detected": _make_detected_condition(BinarySensorDeviceClass.SMOKE),
|
||||||
|
"is_smoke_cleared": _make_cleared_condition(BinarySensorDeviceClass.SMOKE),
|
||||||
|
# Numerical sensor conditions with unit conversion
|
||||||
|
"is_co_value": make_entity_numerical_condition_with_unit(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
CarbonMonoxideConcentrationConverter,
|
||||||
|
),
|
||||||
|
"is_ozone_value": make_entity_numerical_condition_with_unit(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
OzoneConcentrationConverter,
|
||||||
|
),
|
||||||
|
"is_voc_value": make_entity_numerical_condition_with_unit(
|
||||||
|
{
|
||||||
|
SENSOR_DOMAIN: DomainSpec(
|
||||||
|
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
||||||
|
)
|
||||||
|
},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
MassVolumeConcentrationConverter,
|
||||||
|
),
|
||||||
|
"is_voc_ratio_value": make_entity_numerical_condition_with_unit(
|
||||||
|
{
|
||||||
|
SENSOR_DOMAIN: DomainSpec(
|
||||||
|
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
||||||
|
)
|
||||||
|
},
|
||||||
|
CONCENTRATION_PARTS_PER_BILLION,
|
||||||
|
UnitlessRatioConverter,
|
||||||
|
),
|
||||||
|
"is_no_value": make_entity_numerical_condition_with_unit(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_MONOXIDE)},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
NitrogenMonoxideConcentrationConverter,
|
||||||
|
),
|
||||||
|
"is_no2_value": make_entity_numerical_condition_with_unit(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
NitrogenDioxideConcentrationConverter,
|
||||||
|
),
|
||||||
|
"is_so2_value": make_entity_numerical_condition_with_unit(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
SulphurDioxideConcentrationConverter,
|
||||||
|
),
|
||||||
|
# Numerical sensor conditions without unit conversion (single-unit device classes)
|
||||||
|
"is_co2_value": make_entity_numerical_condition(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||||
|
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
),
|
||||||
|
"is_pm1_value": make_entity_numerical_condition(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||||
|
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
),
|
||||||
|
"is_pm25_value": make_entity_numerical_condition(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||||
|
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
),
|
||||||
|
"is_pm4_value": make_entity_numerical_condition(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||||
|
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
),
|
||||||
|
"is_pm10_value": make_entity_numerical_condition(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||||
|
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
),
|
||||||
|
"is_n2o_value": make_entity_numerical_condition(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
|
||||||
|
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||||
|
"""Return the air quality conditions."""
|
||||||
|
return CONDITIONS
|
||||||
@@ -0,0 +1,449 @@
|
|||||||
|
# --- Common condition fields ---
|
||||||
|
|
||||||
|
.condition_behavior: &condition_behavior
|
||||||
|
required: true
|
||||||
|
default: any
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
translation_key: condition_behavior
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- any
|
||||||
|
|
||||||
|
# --- Unit lists for multi-unit pollutants ---
|
||||||
|
|
||||||
|
.co_units: &co_units
|
||||||
|
- "ppb"
|
||||||
|
- "ppm"
|
||||||
|
- "mg/m³"
|
||||||
|
- "μg/m³"
|
||||||
|
|
||||||
|
.ozone_units: &ozone_units
|
||||||
|
- "ppb"
|
||||||
|
- "ppm"
|
||||||
|
- "μg/m³"
|
||||||
|
|
||||||
|
.voc_units: &voc_units
|
||||||
|
- "μg/m³"
|
||||||
|
- "mg/m³"
|
||||||
|
|
||||||
|
.voc_ratio_units: &voc_ratio_units
|
||||||
|
- "ppb"
|
||||||
|
- "ppm"
|
||||||
|
|
||||||
|
.no_units: &no_units
|
||||||
|
- "ppb"
|
||||||
|
- "μg/m³"
|
||||||
|
|
||||||
|
.no2_units: &no2_units
|
||||||
|
- "ppb"
|
||||||
|
- "ppm"
|
||||||
|
- "μg/m³"
|
||||||
|
|
||||||
|
.so2_units: &so2_units
|
||||||
|
- "ppb"
|
||||||
|
- "μg/m³"
|
||||||
|
|
||||||
|
# --- Entity filter anchors ---
|
||||||
|
|
||||||
|
.co_threshold_entity: &co_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: *co_units
|
||||||
|
- domain: sensor
|
||||||
|
device_class: carbon_monoxide
|
||||||
|
- domain: number
|
||||||
|
device_class: carbon_monoxide
|
||||||
|
|
||||||
|
.co2_threshold_entity: &co2_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: "ppm"
|
||||||
|
- domain: sensor
|
||||||
|
device_class: carbon_dioxide
|
||||||
|
- domain: number
|
||||||
|
device_class: carbon_dioxide
|
||||||
|
|
||||||
|
.pm1_threshold_entity: &pm1_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: "μg/m³"
|
||||||
|
- domain: sensor
|
||||||
|
device_class: pm1
|
||||||
|
- domain: number
|
||||||
|
device_class: pm1
|
||||||
|
|
||||||
|
.pm25_threshold_entity: &pm25_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: "μg/m³"
|
||||||
|
- domain: sensor
|
||||||
|
device_class: pm25
|
||||||
|
- domain: number
|
||||||
|
device_class: pm25
|
||||||
|
|
||||||
|
.pm4_threshold_entity: &pm4_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: "μg/m³"
|
||||||
|
- domain: sensor
|
||||||
|
device_class: pm4
|
||||||
|
- domain: number
|
||||||
|
device_class: pm4
|
||||||
|
|
||||||
|
.pm10_threshold_entity: &pm10_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: "μg/m³"
|
||||||
|
- domain: sensor
|
||||||
|
device_class: pm10
|
||||||
|
- domain: number
|
||||||
|
device_class: pm10
|
||||||
|
|
||||||
|
.ozone_threshold_entity: &ozone_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: *ozone_units
|
||||||
|
- domain: sensor
|
||||||
|
device_class: ozone
|
||||||
|
- domain: number
|
||||||
|
device_class: ozone
|
||||||
|
|
||||||
|
.voc_threshold_entity: &voc_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: *voc_units
|
||||||
|
- domain: sensor
|
||||||
|
device_class: volatile_organic_compounds
|
||||||
|
- domain: number
|
||||||
|
device_class: volatile_organic_compounds
|
||||||
|
|
||||||
|
.voc_ratio_threshold_entity: &voc_ratio_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: *voc_ratio_units
|
||||||
|
- domain: sensor
|
||||||
|
device_class: volatile_organic_compounds_parts
|
||||||
|
- domain: number
|
||||||
|
device_class: volatile_organic_compounds_parts
|
||||||
|
|
||||||
|
.no_threshold_entity: &no_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: *no_units
|
||||||
|
- domain: sensor
|
||||||
|
device_class: nitrogen_monoxide
|
||||||
|
- domain: number
|
||||||
|
device_class: nitrogen_monoxide
|
||||||
|
|
||||||
|
.no2_threshold_entity: &no2_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: *no2_units
|
||||||
|
- domain: sensor
|
||||||
|
device_class: nitrogen_dioxide
|
||||||
|
- domain: number
|
||||||
|
device_class: nitrogen_dioxide
|
||||||
|
|
||||||
|
.n2o_threshold_entity: &n2o_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: "μg/m³"
|
||||||
|
- domain: sensor
|
||||||
|
device_class: nitrous_oxide
|
||||||
|
- domain: number
|
||||||
|
device_class: nitrous_oxide
|
||||||
|
|
||||||
|
.so2_threshold_entity: &so2_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: *so2_units
|
||||||
|
- domain: sensor
|
||||||
|
device_class: sulphur_dioxide
|
||||||
|
- domain: number
|
||||||
|
device_class: sulphur_dioxide
|
||||||
|
|
||||||
|
# --- Number anchors for single-unit pollutants ---
|
||||||
|
|
||||||
|
.co2_threshold_number: &co2_threshold_number
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: "ppm"
|
||||||
|
|
||||||
|
.ugm3_threshold_number: &ugm3_threshold_number
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: "μg/m³"
|
||||||
|
|
||||||
|
# --- Binary sensor targets ---
|
||||||
|
|
||||||
|
.target_gas: &target_gas
|
||||||
|
entity:
|
||||||
|
- domain: binary_sensor
|
||||||
|
device_class: gas
|
||||||
|
|
||||||
|
.target_co_binary: &target_co_binary
|
||||||
|
entity:
|
||||||
|
- domain: binary_sensor
|
||||||
|
device_class: carbon_monoxide
|
||||||
|
|
||||||
|
.target_smoke: &target_smoke
|
||||||
|
entity:
|
||||||
|
- domain: binary_sensor
|
||||||
|
device_class: smoke
|
||||||
|
|
||||||
|
# --- Sensor targets ---
|
||||||
|
|
||||||
|
.target_co_sensor: &target_co_sensor
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: carbon_monoxide
|
||||||
|
|
||||||
|
.target_co2: &target_co2
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: carbon_dioxide
|
||||||
|
|
||||||
|
.target_pm1: &target_pm1
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: pm1
|
||||||
|
|
||||||
|
.target_pm25: &target_pm25
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: pm25
|
||||||
|
|
||||||
|
.target_pm4: &target_pm4
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: pm4
|
||||||
|
|
||||||
|
.target_pm10: &target_pm10
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: pm10
|
||||||
|
|
||||||
|
.target_ozone: &target_ozone
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: ozone
|
||||||
|
|
||||||
|
.target_voc: &target_voc
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: volatile_organic_compounds
|
||||||
|
|
||||||
|
.target_voc_ratio: &target_voc_ratio
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: volatile_organic_compounds_parts
|
||||||
|
|
||||||
|
.target_no: &target_no
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: nitrogen_monoxide
|
||||||
|
|
||||||
|
.target_no2: &target_no2
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: nitrogen_dioxide
|
||||||
|
|
||||||
|
.target_n2o: &target_n2o
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: nitrous_oxide
|
||||||
|
|
||||||
|
.target_so2: &target_so2
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: sulphur_dioxide
|
||||||
|
|
||||||
|
# --- Binary sensor conditions ---
|
||||||
|
|
||||||
|
.condition_binary_common: &condition_binary_common
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
|
||||||
|
is_gas_detected:
|
||||||
|
<<: *condition_binary_common
|
||||||
|
target: *target_gas
|
||||||
|
|
||||||
|
is_gas_cleared:
|
||||||
|
<<: *condition_binary_common
|
||||||
|
target: *target_gas
|
||||||
|
|
||||||
|
is_co_detected:
|
||||||
|
<<: *condition_binary_common
|
||||||
|
target: *target_co_binary
|
||||||
|
|
||||||
|
is_co_cleared:
|
||||||
|
<<: *condition_binary_common
|
||||||
|
target: *target_co_binary
|
||||||
|
|
||||||
|
is_smoke_detected:
|
||||||
|
<<: *condition_binary_common
|
||||||
|
target: *target_smoke
|
||||||
|
|
||||||
|
is_smoke_cleared:
|
||||||
|
<<: *condition_binary_common
|
||||||
|
target: *target_smoke
|
||||||
|
|
||||||
|
# --- Numerical sensor conditions with unit conversion ---
|
||||||
|
|
||||||
|
is_co_value:
|
||||||
|
target: *target_co_sensor
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *co_threshold_entity
|
||||||
|
mode: is
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *co_units
|
||||||
|
|
||||||
|
is_ozone_value:
|
||||||
|
target: *target_ozone
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *ozone_threshold_entity
|
||||||
|
mode: is
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *ozone_units
|
||||||
|
|
||||||
|
is_voc_value:
|
||||||
|
target: *target_voc
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *voc_threshold_entity
|
||||||
|
mode: is
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *voc_units
|
||||||
|
|
||||||
|
is_voc_ratio_value:
|
||||||
|
target: *target_voc_ratio
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *voc_ratio_threshold_entity
|
||||||
|
mode: is
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *voc_ratio_units
|
||||||
|
|
||||||
|
is_no_value:
|
||||||
|
target: *target_no
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *no_threshold_entity
|
||||||
|
mode: is
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *no_units
|
||||||
|
|
||||||
|
is_no2_value:
|
||||||
|
target: *target_no2
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *no2_threshold_entity
|
||||||
|
mode: is
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *no2_units
|
||||||
|
|
||||||
|
is_so2_value:
|
||||||
|
target: *target_so2
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *so2_threshold_entity
|
||||||
|
mode: is
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *so2_units
|
||||||
|
|
||||||
|
# --- Numerical sensor conditions without unit conversion ---
|
||||||
|
|
||||||
|
is_co2_value:
|
||||||
|
target: *target_co2
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *co2_threshold_entity
|
||||||
|
mode: is
|
||||||
|
number: *co2_threshold_number
|
||||||
|
|
||||||
|
is_pm1_value:
|
||||||
|
target: *target_pm1
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *pm1_threshold_entity
|
||||||
|
mode: is
|
||||||
|
number: *ugm3_threshold_number
|
||||||
|
|
||||||
|
is_pm25_value:
|
||||||
|
target: *target_pm25
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *pm25_threshold_entity
|
||||||
|
mode: is
|
||||||
|
number: *ugm3_threshold_number
|
||||||
|
|
||||||
|
is_pm4_value:
|
||||||
|
target: *target_pm4
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *pm4_threshold_entity
|
||||||
|
mode: is
|
||||||
|
number: *ugm3_threshold_number
|
||||||
|
|
||||||
|
is_pm10_value:
|
||||||
|
target: *target_pm10
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *pm10_threshold_entity
|
||||||
|
mode: is
|
||||||
|
number: *ugm3_threshold_number
|
||||||
|
|
||||||
|
is_n2o_value:
|
||||||
|
target: *target_n2o
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *n2o_threshold_entity
|
||||||
|
mode: is
|
||||||
|
number: *ugm3_threshold_number
|
||||||
@@ -1,7 +1,164 @@
|
|||||||
{
|
{
|
||||||
|
"conditions": {
|
||||||
|
"is_co2_value": {
|
||||||
|
"condition": "mdi:molecule-co2"
|
||||||
|
},
|
||||||
|
"is_co_cleared": {
|
||||||
|
"condition": "mdi:check-circle"
|
||||||
|
},
|
||||||
|
"is_co_detected": {
|
||||||
|
"condition": "mdi:molecule-co"
|
||||||
|
},
|
||||||
|
"is_co_value": {
|
||||||
|
"condition": "mdi:molecule-co"
|
||||||
|
},
|
||||||
|
"is_gas_cleared": {
|
||||||
|
"condition": "mdi:check-circle"
|
||||||
|
},
|
||||||
|
"is_gas_detected": {
|
||||||
|
"condition": "mdi:gas-cylinder"
|
||||||
|
},
|
||||||
|
"is_n2o_value": {
|
||||||
|
"condition": "mdi:factory"
|
||||||
|
},
|
||||||
|
"is_no2_value": {
|
||||||
|
"condition": "mdi:factory"
|
||||||
|
},
|
||||||
|
"is_no_value": {
|
||||||
|
"condition": "mdi:factory"
|
||||||
|
},
|
||||||
|
"is_ozone_value": {
|
||||||
|
"condition": "mdi:weather-sunny-alert"
|
||||||
|
},
|
||||||
|
"is_pm10_value": {
|
||||||
|
"condition": "mdi:blur"
|
||||||
|
},
|
||||||
|
"is_pm1_value": {
|
||||||
|
"condition": "mdi:blur"
|
||||||
|
},
|
||||||
|
"is_pm25_value": {
|
||||||
|
"condition": "mdi:blur"
|
||||||
|
},
|
||||||
|
"is_pm4_value": {
|
||||||
|
"condition": "mdi:blur"
|
||||||
|
},
|
||||||
|
"is_smoke_cleared": {
|
||||||
|
"condition": "mdi:check-circle"
|
||||||
|
},
|
||||||
|
"is_smoke_detected": {
|
||||||
|
"condition": "mdi:smoke-detector-variant"
|
||||||
|
},
|
||||||
|
"is_so2_value": {
|
||||||
|
"condition": "mdi:factory"
|
||||||
|
},
|
||||||
|
"is_voc_ratio_value": {
|
||||||
|
"condition": "mdi:air-filter"
|
||||||
|
},
|
||||||
|
"is_voc_value": {
|
||||||
|
"condition": "mdi:air-filter"
|
||||||
|
}
|
||||||
|
},
|
||||||
"entity_component": {
|
"entity_component": {
|
||||||
"_": {
|
"_": {
|
||||||
"default": "mdi:air-filter"
|
"default": "mdi:air-filter"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"triggers": {
|
||||||
|
"co2_changed": {
|
||||||
|
"trigger": "mdi:molecule-co2"
|
||||||
|
},
|
||||||
|
"co2_crossed_threshold": {
|
||||||
|
"trigger": "mdi:molecule-co2"
|
||||||
|
},
|
||||||
|
"co_changed": {
|
||||||
|
"trigger": "mdi:molecule-co"
|
||||||
|
},
|
||||||
|
"co_cleared": {
|
||||||
|
"trigger": "mdi:check-circle"
|
||||||
|
},
|
||||||
|
"co_crossed_threshold": {
|
||||||
|
"trigger": "mdi:molecule-co"
|
||||||
|
},
|
||||||
|
"co_detected": {
|
||||||
|
"trigger": "mdi:molecule-co"
|
||||||
|
},
|
||||||
|
"gas_cleared": {
|
||||||
|
"trigger": "mdi:check-circle"
|
||||||
|
},
|
||||||
|
"gas_detected": {
|
||||||
|
"trigger": "mdi:gas-cylinder"
|
||||||
|
},
|
||||||
|
"n2o_changed": {
|
||||||
|
"trigger": "mdi:factory"
|
||||||
|
},
|
||||||
|
"n2o_crossed_threshold": {
|
||||||
|
"trigger": "mdi:factory"
|
||||||
|
},
|
||||||
|
"no2_changed": {
|
||||||
|
"trigger": "mdi:factory"
|
||||||
|
},
|
||||||
|
"no2_crossed_threshold": {
|
||||||
|
"trigger": "mdi:factory"
|
||||||
|
},
|
||||||
|
"no_changed": {
|
||||||
|
"trigger": "mdi:factory"
|
||||||
|
},
|
||||||
|
"no_crossed_threshold": {
|
||||||
|
"trigger": "mdi:factory"
|
||||||
|
},
|
||||||
|
"ozone_changed": {
|
||||||
|
"trigger": "mdi:weather-sunny-alert"
|
||||||
|
},
|
||||||
|
"ozone_crossed_threshold": {
|
||||||
|
"trigger": "mdi:weather-sunny-alert"
|
||||||
|
},
|
||||||
|
"pm10_changed": {
|
||||||
|
"trigger": "mdi:blur"
|
||||||
|
},
|
||||||
|
"pm10_crossed_threshold": {
|
||||||
|
"trigger": "mdi:blur"
|
||||||
|
},
|
||||||
|
"pm1_changed": {
|
||||||
|
"trigger": "mdi:blur"
|
||||||
|
},
|
||||||
|
"pm1_crossed_threshold": {
|
||||||
|
"trigger": "mdi:blur"
|
||||||
|
},
|
||||||
|
"pm25_changed": {
|
||||||
|
"trigger": "mdi:blur"
|
||||||
|
},
|
||||||
|
"pm25_crossed_threshold": {
|
||||||
|
"trigger": "mdi:blur"
|
||||||
|
},
|
||||||
|
"pm4_changed": {
|
||||||
|
"trigger": "mdi:blur"
|
||||||
|
},
|
||||||
|
"pm4_crossed_threshold": {
|
||||||
|
"trigger": "mdi:blur"
|
||||||
|
},
|
||||||
|
"smoke_cleared": {
|
||||||
|
"trigger": "mdi:check-circle"
|
||||||
|
},
|
||||||
|
"smoke_detected": {
|
||||||
|
"trigger": "mdi:smoke-detector-variant"
|
||||||
|
},
|
||||||
|
"so2_changed": {
|
||||||
|
"trigger": "mdi:factory"
|
||||||
|
},
|
||||||
|
"so2_crossed_threshold": {
|
||||||
|
"trigger": "mdi:factory"
|
||||||
|
},
|
||||||
|
"voc_changed": {
|
||||||
|
"trigger": "mdi:air-filter"
|
||||||
|
},
|
||||||
|
"voc_crossed_threshold": {
|
||||||
|
"trigger": "mdi:air-filter"
|
||||||
|
},
|
||||||
|
"voc_ratio_changed": {
|
||||||
|
"trigger": "mdi:air-filter"
|
||||||
|
},
|
||||||
|
"voc_ratio_crossed_threshold": {
|
||||||
|
"trigger": "mdi:air-filter"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,565 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"condition_behavior_name": "Condition passes if",
|
||||||
|
"condition_threshold_name": "Threshold type",
|
||||||
|
"trigger_behavior_name": "Trigger when",
|
||||||
|
"trigger_threshold_name": "Threshold type"
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"is_co2_value": {
|
||||||
|
"description": "Tests the carbon dioxide level of one or more entities.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Carbon dioxide value"
|
||||||
|
},
|
||||||
|
"is_co_cleared": {
|
||||||
|
"description": "Tests if one or more carbon monoxide sensors are cleared.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Carbon monoxide cleared"
|
||||||
|
},
|
||||||
|
"is_co_detected": {
|
||||||
|
"description": "Tests if one or more carbon monoxide sensors are detecting carbon monoxide.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Carbon monoxide detected"
|
||||||
|
},
|
||||||
|
"is_co_value": {
|
||||||
|
"description": "Tests the carbon monoxide level of one or more entities.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Carbon monoxide value"
|
||||||
|
},
|
||||||
|
"is_gas_cleared": {
|
||||||
|
"description": "Tests if one or more gas sensors are cleared.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Gas cleared"
|
||||||
|
},
|
||||||
|
"is_gas_detected": {
|
||||||
|
"description": "Tests if one or more gas sensors are detecting gas.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Gas detected"
|
||||||
|
},
|
||||||
|
"is_n2o_value": {
|
||||||
|
"description": "Tests the nitrous oxide level of one or more entities.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Nitrous oxide value"
|
||||||
|
},
|
||||||
|
"is_no2_value": {
|
||||||
|
"description": "Tests the nitrogen dioxide level of one or more entities.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Nitrogen dioxide value"
|
||||||
|
},
|
||||||
|
"is_no_value": {
|
||||||
|
"description": "Tests the nitrogen monoxide level of one or more entities.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Nitrogen monoxide value"
|
||||||
|
},
|
||||||
|
"is_ozone_value": {
|
||||||
|
"description": "Tests the ozone level of one or more entities.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Ozone value"
|
||||||
|
},
|
||||||
|
"is_pm10_value": {
|
||||||
|
"description": "Tests the PM10 level of one or more entities.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "PM10 value"
|
||||||
|
},
|
||||||
|
"is_pm1_value": {
|
||||||
|
"description": "Tests the PM1 level of one or more entities.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "PM1 value"
|
||||||
|
},
|
||||||
|
"is_pm25_value": {
|
||||||
|
"description": "Tests the PM2.5 level of one or more entities.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "PM2.5 value"
|
||||||
|
},
|
||||||
|
"is_pm4_value": {
|
||||||
|
"description": "Tests the PM4 level of one or more entities.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "PM4 value"
|
||||||
|
},
|
||||||
|
"is_smoke_cleared": {
|
||||||
|
"description": "Tests if one or more smoke sensors are cleared.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Smoke cleared"
|
||||||
|
},
|
||||||
|
"is_smoke_detected": {
|
||||||
|
"description": "Tests if one or more smoke sensors are detecting smoke.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Smoke detected"
|
||||||
|
},
|
||||||
|
"is_so2_value": {
|
||||||
|
"description": "Tests the sulphur dioxide level of one or more entities.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Sulphur dioxide value"
|
||||||
|
},
|
||||||
|
"is_voc_ratio_value": {
|
||||||
|
"description": "Tests the volatile organic compounds ratio of one or more entities.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Volatile organic compounds ratio value"
|
||||||
|
},
|
||||||
|
"is_voc_value": {
|
||||||
|
"description": "Tests the volatile organic compounds level of one or more entities.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Volatile organic compounds value"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selector": {
|
||||||
|
"condition_behavior": {
|
||||||
|
"options": {
|
||||||
|
"all": "All",
|
||||||
|
"any": "Any"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trigger_behavior": {
|
||||||
|
"options": {
|
||||||
|
"any": "Any",
|
||||||
|
"first": "First",
|
||||||
|
"last": "Last"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Air Quality",
|
||||||
|
"triggers": {
|
||||||
|
"co2_changed": {
|
||||||
|
"description": "Triggers after one or more carbon dioxide levels change.",
|
||||||
|
"fields": {
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Carbon dioxide level changed"
|
||||||
|
},
|
||||||
|
"co2_crossed_threshold": {
|
||||||
|
"description": "Triggers after one or more carbon dioxide levels cross a threshold.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Carbon dioxide level crossed threshold"
|
||||||
|
},
|
||||||
|
"co_changed": {
|
||||||
|
"description": "Triggers after one or more carbon monoxide levels change.",
|
||||||
|
"fields": {
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Carbon monoxide level changed"
|
||||||
|
},
|
||||||
|
"co_cleared": {
|
||||||
|
"description": "Triggers after one or more carbon monoxide sensors stop detecting carbon monoxide.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Carbon monoxide cleared"
|
||||||
|
},
|
||||||
|
"co_crossed_threshold": {
|
||||||
|
"description": "Triggers after one or more carbon monoxide levels cross a threshold.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Carbon monoxide level crossed threshold"
|
||||||
|
},
|
||||||
|
"co_detected": {
|
||||||
|
"description": "Triggers after one or more carbon monoxide sensors start detecting carbon monoxide.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Carbon monoxide detected"
|
||||||
|
},
|
||||||
|
"gas_cleared": {
|
||||||
|
"description": "Triggers after one or more gas sensors stop detecting gas.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Gas cleared"
|
||||||
|
},
|
||||||
|
"gas_detected": {
|
||||||
|
"description": "Triggers after one or more gas sensors start detecting gas.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Gas detected"
|
||||||
|
},
|
||||||
|
"n2o_changed": {
|
||||||
|
"description": "Triggers after one or more nitrous oxide levels change.",
|
||||||
|
"fields": {
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Nitrous oxide level changed"
|
||||||
|
},
|
||||||
|
"n2o_crossed_threshold": {
|
||||||
|
"description": "Triggers after one or more nitrous oxide levels cross a threshold.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Nitrous oxide level crossed threshold"
|
||||||
|
},
|
||||||
|
"no2_changed": {
|
||||||
|
"description": "Triggers after one or more nitrogen dioxide levels change.",
|
||||||
|
"fields": {
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Nitrogen dioxide level changed"
|
||||||
|
},
|
||||||
|
"no2_crossed_threshold": {
|
||||||
|
"description": "Triggers after one or more nitrogen dioxide levels cross a threshold.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Nitrogen dioxide level crossed threshold"
|
||||||
|
},
|
||||||
|
"no_changed": {
|
||||||
|
"description": "Triggers after one or more nitrogen monoxide levels change.",
|
||||||
|
"fields": {
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Nitrogen monoxide level changed"
|
||||||
|
},
|
||||||
|
"no_crossed_threshold": {
|
||||||
|
"description": "Triggers after one or more nitrogen monoxide levels cross a threshold.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Nitrogen monoxide level crossed threshold"
|
||||||
|
},
|
||||||
|
"ozone_changed": {
|
||||||
|
"description": "Triggers after one or more ozone levels change.",
|
||||||
|
"fields": {
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Ozone level changed"
|
||||||
|
},
|
||||||
|
"ozone_crossed_threshold": {
|
||||||
|
"description": "Triggers after one or more ozone levels cross a threshold.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Ozone level crossed threshold"
|
||||||
|
},
|
||||||
|
"pm10_changed": {
|
||||||
|
"description": "Triggers after one or more PM10 levels change.",
|
||||||
|
"fields": {
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "PM10 level changed"
|
||||||
|
},
|
||||||
|
"pm10_crossed_threshold": {
|
||||||
|
"description": "Triggers after one or more PM10 levels cross a threshold.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "PM10 level crossed threshold"
|
||||||
|
},
|
||||||
|
"pm1_changed": {
|
||||||
|
"description": "Triggers after one or more PM1 levels change.",
|
||||||
|
"fields": {
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "PM1 level changed"
|
||||||
|
},
|
||||||
|
"pm1_crossed_threshold": {
|
||||||
|
"description": "Triggers after one or more PM1 levels cross a threshold.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "PM1 level crossed threshold"
|
||||||
|
},
|
||||||
|
"pm25_changed": {
|
||||||
|
"description": "Triggers after one or more PM2.5 levels change.",
|
||||||
|
"fields": {
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "PM2.5 level changed"
|
||||||
|
},
|
||||||
|
"pm25_crossed_threshold": {
|
||||||
|
"description": "Triggers after one or more PM2.5 levels cross a threshold.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "PM2.5 level crossed threshold"
|
||||||
|
},
|
||||||
|
"pm4_changed": {
|
||||||
|
"description": "Triggers after one or more PM4 levels change.",
|
||||||
|
"fields": {
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "PM4 level changed"
|
||||||
|
},
|
||||||
|
"pm4_crossed_threshold": {
|
||||||
|
"description": "Triggers after one or more PM4 levels cross a threshold.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "PM4 level crossed threshold"
|
||||||
|
},
|
||||||
|
"smoke_cleared": {
|
||||||
|
"description": "Triggers after one or more smoke sensors stop detecting smoke.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Smoke cleared"
|
||||||
|
},
|
||||||
|
"smoke_detected": {
|
||||||
|
"description": "Triggers after one or more smoke sensors start detecting smoke.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Smoke detected"
|
||||||
|
},
|
||||||
|
"so2_changed": {
|
||||||
|
"description": "Triggers after one or more sulphur dioxide levels change.",
|
||||||
|
"fields": {
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Sulphur dioxide level changed"
|
||||||
|
},
|
||||||
|
"so2_crossed_threshold": {
|
||||||
|
"description": "Triggers after one or more sulphur dioxide levels cross a threshold.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Sulphur dioxide level crossed threshold"
|
||||||
|
},
|
||||||
|
"voc_changed": {
|
||||||
|
"description": "Triggers after one or more volatile organic compound levels change.",
|
||||||
|
"fields": {
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Volatile organic compounds level changed"
|
||||||
|
},
|
||||||
|
"voc_crossed_threshold": {
|
||||||
|
"description": "Triggers after one or more volatile organic compounds levels cross a threshold.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Volatile organic compounds level crossed threshold"
|
||||||
|
},
|
||||||
|
"voc_ratio_changed": {
|
||||||
|
"description": "Triggers after one or more volatile organic compound ratios change.",
|
||||||
|
"fields": {
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Volatile organic compounds ratio changed"
|
||||||
|
},
|
||||||
|
"voc_ratio_crossed_threshold": {
|
||||||
|
"description": "Triggers after one or more volatile organic compounds ratios cross a threshold.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::air_quality::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Volatile organic compounds ratio crossed threshold"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
"""Provides triggers for air quality."""
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import (
|
||||||
|
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||||
|
BinarySensorDeviceClass,
|
||||||
|
)
|
||||||
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
CONCENTRATION_PARTS_PER_BILLION,
|
||||||
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_ON,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.automation import DomainSpec
|
||||||
|
from homeassistant.helpers.trigger import (
|
||||||
|
EntityTargetStateTriggerBase,
|
||||||
|
Trigger,
|
||||||
|
make_entity_numerical_state_changed_trigger,
|
||||||
|
make_entity_numerical_state_changed_with_unit_trigger,
|
||||||
|
make_entity_numerical_state_crossed_threshold_trigger,
|
||||||
|
make_entity_numerical_state_crossed_threshold_with_unit_trigger,
|
||||||
|
make_entity_target_state_trigger,
|
||||||
|
)
|
||||||
|
from homeassistant.util.unit_conversion import (
|
||||||
|
CarbonMonoxideConcentrationConverter,
|
||||||
|
MassVolumeConcentrationConverter,
|
||||||
|
NitrogenDioxideConcentrationConverter,
|
||||||
|
NitrogenMonoxideConcentrationConverter,
|
||||||
|
OzoneConcentrationConverter,
|
||||||
|
SulphurDioxideConcentrationConverter,
|
||||||
|
UnitlessRatioConverter,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_detected_trigger(
|
||||||
|
device_class: BinarySensorDeviceClass,
|
||||||
|
) -> type[EntityTargetStateTriggerBase]:
|
||||||
|
"""Create a detected trigger for a binary sensor device class."""
|
||||||
|
|
||||||
|
return make_entity_target_state_trigger(
|
||||||
|
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_ON
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_cleared_trigger(
|
||||||
|
device_class: BinarySensorDeviceClass,
|
||||||
|
) -> type[EntityTargetStateTriggerBase]:
|
||||||
|
"""Create a cleared trigger for a binary sensor device class."""
|
||||||
|
|
||||||
|
return make_entity_target_state_trigger(
|
||||||
|
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_OFF
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
TRIGGERS: dict[str, type[Trigger]] = {
|
||||||
|
# Binary sensor triggers (detected/cleared)
|
||||||
|
"gas_detected": _make_detected_trigger(BinarySensorDeviceClass.GAS),
|
||||||
|
"gas_cleared": _make_cleared_trigger(BinarySensorDeviceClass.GAS),
|
||||||
|
"co_detected": _make_detected_trigger(BinarySensorDeviceClass.CO),
|
||||||
|
"co_cleared": _make_cleared_trigger(BinarySensorDeviceClass.CO),
|
||||||
|
"smoke_detected": _make_detected_trigger(BinarySensorDeviceClass.SMOKE),
|
||||||
|
"smoke_cleared": _make_cleared_trigger(BinarySensorDeviceClass.SMOKE),
|
||||||
|
# Numerical sensor triggers with unit conversion
|
||||||
|
"co_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
CarbonMonoxideConcentrationConverter,
|
||||||
|
),
|
||||||
|
"co_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
CarbonMonoxideConcentrationConverter,
|
||||||
|
),
|
||||||
|
"ozone_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
OzoneConcentrationConverter,
|
||||||
|
),
|
||||||
|
"ozone_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
OzoneConcentrationConverter,
|
||||||
|
),
|
||||||
|
"voc_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||||
|
{
|
||||||
|
SENSOR_DOMAIN: DomainSpec(
|
||||||
|
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
||||||
|
)
|
||||||
|
},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
MassVolumeConcentrationConverter,
|
||||||
|
),
|
||||||
|
"voc_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||||
|
{
|
||||||
|
SENSOR_DOMAIN: DomainSpec(
|
||||||
|
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
||||||
|
)
|
||||||
|
},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
MassVolumeConcentrationConverter,
|
||||||
|
),
|
||||||
|
"voc_ratio_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||||
|
{
|
||||||
|
SENSOR_DOMAIN: DomainSpec(
|
||||||
|
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
||||||
|
)
|
||||||
|
},
|
||||||
|
CONCENTRATION_PARTS_PER_BILLION,
|
||||||
|
UnitlessRatioConverter,
|
||||||
|
),
|
||||||
|
"voc_ratio_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||||
|
{
|
||||||
|
SENSOR_DOMAIN: DomainSpec(
|
||||||
|
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
||||||
|
)
|
||||||
|
},
|
||||||
|
CONCENTRATION_PARTS_PER_BILLION,
|
||||||
|
UnitlessRatioConverter,
|
||||||
|
),
|
||||||
|
"no_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_MONOXIDE)},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
NitrogenMonoxideConcentrationConverter,
|
||||||
|
),
|
||||||
|
"no_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_MONOXIDE)},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
NitrogenMonoxideConcentrationConverter,
|
||||||
|
),
|
||||||
|
"no2_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
NitrogenDioxideConcentrationConverter,
|
||||||
|
),
|
||||||
|
"no2_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
NitrogenDioxideConcentrationConverter,
|
||||||
|
),
|
||||||
|
"so2_changed": make_entity_numerical_state_changed_with_unit_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
SulphurDioxideConcentrationConverter,
|
||||||
|
),
|
||||||
|
"so2_crossed_threshold": make_entity_numerical_state_crossed_threshold_with_unit_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
SulphurDioxideConcentrationConverter,
|
||||||
|
),
|
||||||
|
# Numerical sensor triggers without unit conversion (single-unit device classes)
|
||||||
|
"co2_changed": make_entity_numerical_state_changed_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||||
|
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
),
|
||||||
|
"co2_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||||
|
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
),
|
||||||
|
"pm1_changed": make_entity_numerical_state_changed_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||||
|
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
),
|
||||||
|
"pm1_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||||
|
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
),
|
||||||
|
"pm25_changed": make_entity_numerical_state_changed_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||||
|
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
),
|
||||||
|
"pm25_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||||
|
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
),
|
||||||
|
"pm4_changed": make_entity_numerical_state_changed_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||||
|
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
),
|
||||||
|
"pm4_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||||
|
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
),
|
||||||
|
"pm10_changed": make_entity_numerical_state_changed_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||||
|
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
),
|
||||||
|
"pm10_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||||
|
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
),
|
||||||
|
"n2o_changed": make_entity_numerical_state_changed_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
|
||||||
|
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
),
|
||||||
|
"n2o_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||||
|
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
|
||||||
|
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||||
|
"""Return the triggers for air quality."""
|
||||||
|
return TRIGGERS
|
||||||
@@ -0,0 +1,617 @@
|
|||||||
|
.trigger_common_fields:
|
||||||
|
behavior: &trigger_behavior
|
||||||
|
required: true
|
||||||
|
default: any
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
translation_key: trigger_behavior
|
||||||
|
options:
|
||||||
|
- first
|
||||||
|
- last
|
||||||
|
- any
|
||||||
|
|
||||||
|
# --- Unit lists for multi-unit pollutants ---
|
||||||
|
|
||||||
|
.co_units: &co_units
|
||||||
|
- "ppb"
|
||||||
|
- "ppm"
|
||||||
|
- "mg/m³"
|
||||||
|
- "μg/m³"
|
||||||
|
|
||||||
|
.ozone_units: &ozone_units
|
||||||
|
- "ppb"
|
||||||
|
- "ppm"
|
||||||
|
- "μg/m³"
|
||||||
|
|
||||||
|
.voc_units: &voc_units
|
||||||
|
- "μg/m³"
|
||||||
|
- "mg/m³"
|
||||||
|
|
||||||
|
.voc_ratio_units: &voc_ratio_units
|
||||||
|
- "ppb"
|
||||||
|
- "ppm"
|
||||||
|
|
||||||
|
.no_units: &no_units
|
||||||
|
- "ppb"
|
||||||
|
- "μg/m³"
|
||||||
|
|
||||||
|
.no2_units: &no2_units
|
||||||
|
- "ppb"
|
||||||
|
- "ppm"
|
||||||
|
- "μg/m³"
|
||||||
|
|
||||||
|
.so2_units: &so2_units
|
||||||
|
- "ppb"
|
||||||
|
- "μg/m³"
|
||||||
|
|
||||||
|
# --- Entity filter anchors ---
|
||||||
|
|
||||||
|
.co_threshold_entity: &co_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: *co_units
|
||||||
|
- domain: sensor
|
||||||
|
device_class: carbon_monoxide
|
||||||
|
- domain: number
|
||||||
|
device_class: carbon_monoxide
|
||||||
|
|
||||||
|
.co2_threshold_entity: &co2_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: "ppm"
|
||||||
|
- domain: sensor
|
||||||
|
device_class: carbon_dioxide
|
||||||
|
- domain: number
|
||||||
|
device_class: carbon_dioxide
|
||||||
|
|
||||||
|
.pm1_threshold_entity: &pm1_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: "μg/m³"
|
||||||
|
- domain: sensor
|
||||||
|
device_class: pm1
|
||||||
|
- domain: number
|
||||||
|
device_class: pm1
|
||||||
|
|
||||||
|
.pm25_threshold_entity: &pm25_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: "μg/m³"
|
||||||
|
- domain: sensor
|
||||||
|
device_class: pm25
|
||||||
|
- domain: number
|
||||||
|
device_class: pm25
|
||||||
|
|
||||||
|
.pm4_threshold_entity: &pm4_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: "μg/m³"
|
||||||
|
- domain: sensor
|
||||||
|
device_class: pm4
|
||||||
|
- domain: number
|
||||||
|
device_class: pm4
|
||||||
|
|
||||||
|
.pm10_threshold_entity: &pm10_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: "μg/m³"
|
||||||
|
- domain: sensor
|
||||||
|
device_class: pm10
|
||||||
|
- domain: number
|
||||||
|
device_class: pm10
|
||||||
|
|
||||||
|
.ozone_threshold_entity: &ozone_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: *ozone_units
|
||||||
|
- domain: sensor
|
||||||
|
device_class: ozone
|
||||||
|
- domain: number
|
||||||
|
device_class: ozone
|
||||||
|
|
||||||
|
.voc_threshold_entity: &voc_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: *voc_units
|
||||||
|
- domain: sensor
|
||||||
|
device_class: volatile_organic_compounds
|
||||||
|
- domain: number
|
||||||
|
device_class: volatile_organic_compounds
|
||||||
|
|
||||||
|
.voc_ratio_threshold_entity: &voc_ratio_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: *voc_ratio_units
|
||||||
|
- domain: sensor
|
||||||
|
device_class: volatile_organic_compounds_parts
|
||||||
|
- domain: number
|
||||||
|
device_class: volatile_organic_compounds_parts
|
||||||
|
|
||||||
|
.no_threshold_entity: &no_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: *no_units
|
||||||
|
- domain: sensor
|
||||||
|
device_class: nitrogen_monoxide
|
||||||
|
- domain: number
|
||||||
|
device_class: nitrogen_monoxide
|
||||||
|
|
||||||
|
.no2_threshold_entity: &no2_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: *no2_units
|
||||||
|
- domain: sensor
|
||||||
|
device_class: nitrogen_dioxide
|
||||||
|
- domain: number
|
||||||
|
device_class: nitrogen_dioxide
|
||||||
|
|
||||||
|
.n2o_threshold_entity: &n2o_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: "μg/m³"
|
||||||
|
- domain: sensor
|
||||||
|
device_class: nitrous_oxide
|
||||||
|
- domain: number
|
||||||
|
device_class: nitrous_oxide
|
||||||
|
|
||||||
|
.so2_threshold_entity: &so2_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: *so2_units
|
||||||
|
- domain: sensor
|
||||||
|
device_class: sulphur_dioxide
|
||||||
|
- domain: number
|
||||||
|
device_class: sulphur_dioxide
|
||||||
|
|
||||||
|
# --- Number anchors for single-unit pollutants ---
|
||||||
|
|
||||||
|
.co2_threshold_number: &co2_threshold_number
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: "ppm"
|
||||||
|
|
||||||
|
.ugm3_threshold_number: &ugm3_threshold_number
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: "μg/m³"
|
||||||
|
|
||||||
|
# Binary sensor detected/cleared trigger fields
|
||||||
|
.trigger_binary_fields: &trigger_binary_fields
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
|
||||||
|
# --- Binary sensor targets ---
|
||||||
|
|
||||||
|
.target_gas: &target_gas
|
||||||
|
entity:
|
||||||
|
- domain: binary_sensor
|
||||||
|
device_class: gas
|
||||||
|
|
||||||
|
.target_co_binary: &target_co_binary
|
||||||
|
entity:
|
||||||
|
- domain: binary_sensor
|
||||||
|
device_class: carbon_monoxide
|
||||||
|
|
||||||
|
.target_smoke: &target_smoke
|
||||||
|
entity:
|
||||||
|
- domain: binary_sensor
|
||||||
|
device_class: smoke
|
||||||
|
|
||||||
|
# --- Sensor targets ---
|
||||||
|
|
||||||
|
.target_co_sensor: &target_co_sensor
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: carbon_monoxide
|
||||||
|
|
||||||
|
.target_co2: &target_co2
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: carbon_dioxide
|
||||||
|
|
||||||
|
.target_pm1: &target_pm1
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: pm1
|
||||||
|
|
||||||
|
.target_pm25: &target_pm25
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: pm25
|
||||||
|
|
||||||
|
.target_pm4: &target_pm4
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: pm4
|
||||||
|
|
||||||
|
.target_pm10: &target_pm10
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: pm10
|
||||||
|
|
||||||
|
.target_ozone: &target_ozone
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: ozone
|
||||||
|
|
||||||
|
.target_voc: &target_voc
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: volatile_organic_compounds
|
||||||
|
|
||||||
|
.target_voc_ratio: &target_voc_ratio
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: volatile_organic_compounds_parts
|
||||||
|
|
||||||
|
.target_no: &target_no
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: nitrogen_monoxide
|
||||||
|
|
||||||
|
.target_no2: &target_no2
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: nitrogen_dioxide
|
||||||
|
|
||||||
|
.target_n2o: &target_n2o
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: nitrous_oxide
|
||||||
|
|
||||||
|
.target_so2: &target_so2
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: sulphur_dioxide
|
||||||
|
|
||||||
|
# --- Binary sensor triggers ---
|
||||||
|
|
||||||
|
gas_detected:
|
||||||
|
fields: *trigger_binary_fields
|
||||||
|
target: *target_gas
|
||||||
|
|
||||||
|
gas_cleared:
|
||||||
|
fields: *trigger_binary_fields
|
||||||
|
target: *target_gas
|
||||||
|
|
||||||
|
co_detected:
|
||||||
|
fields: *trigger_binary_fields
|
||||||
|
target: *target_co_binary
|
||||||
|
|
||||||
|
co_cleared:
|
||||||
|
fields: *trigger_binary_fields
|
||||||
|
target: *target_co_binary
|
||||||
|
|
||||||
|
smoke_detected:
|
||||||
|
fields: *trigger_binary_fields
|
||||||
|
target: *target_smoke
|
||||||
|
|
||||||
|
smoke_cleared:
|
||||||
|
fields: *trigger_binary_fields
|
||||||
|
target: *target_smoke
|
||||||
|
|
||||||
|
# --- Numerical sensor triggers ---
|
||||||
|
|
||||||
|
# CO (multi-unit)
|
||||||
|
co_changed:
|
||||||
|
target: *target_co_sensor
|
||||||
|
fields:
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *co_threshold_entity
|
||||||
|
mode: changed
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *co_units
|
||||||
|
|
||||||
|
co_crossed_threshold:
|
||||||
|
target: *target_co_sensor
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *co_threshold_entity
|
||||||
|
mode: crossed
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *co_units
|
||||||
|
|
||||||
|
# CO2 (single-unit: ppm)
|
||||||
|
co2_changed:
|
||||||
|
target: *target_co2
|
||||||
|
fields:
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *co2_threshold_entity
|
||||||
|
mode: changed
|
||||||
|
number: *co2_threshold_number
|
||||||
|
|
||||||
|
co2_crossed_threshold:
|
||||||
|
target: *target_co2
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *co2_threshold_entity
|
||||||
|
mode: crossed
|
||||||
|
number: *co2_threshold_number
|
||||||
|
|
||||||
|
# PM1 (single-unit: μg/m³)
|
||||||
|
pm1_changed:
|
||||||
|
target: *target_pm1
|
||||||
|
fields:
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *pm1_threshold_entity
|
||||||
|
mode: changed
|
||||||
|
number: *ugm3_threshold_number
|
||||||
|
|
||||||
|
pm1_crossed_threshold:
|
||||||
|
target: *target_pm1
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *pm1_threshold_entity
|
||||||
|
mode: crossed
|
||||||
|
number: *ugm3_threshold_number
|
||||||
|
|
||||||
|
# PM2.5 (single-unit: μg/m³)
|
||||||
|
pm25_changed:
|
||||||
|
target: *target_pm25
|
||||||
|
fields:
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *pm25_threshold_entity
|
||||||
|
mode: changed
|
||||||
|
number: *ugm3_threshold_number
|
||||||
|
|
||||||
|
pm25_crossed_threshold:
|
||||||
|
target: *target_pm25
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *pm25_threshold_entity
|
||||||
|
mode: crossed
|
||||||
|
number: *ugm3_threshold_number
|
||||||
|
|
||||||
|
# PM4 (single-unit: μg/m³)
|
||||||
|
pm4_changed:
|
||||||
|
target: *target_pm4
|
||||||
|
fields:
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *pm4_threshold_entity
|
||||||
|
mode: changed
|
||||||
|
number: *ugm3_threshold_number
|
||||||
|
|
||||||
|
pm4_crossed_threshold:
|
||||||
|
target: *target_pm4
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *pm4_threshold_entity
|
||||||
|
mode: crossed
|
||||||
|
number: *ugm3_threshold_number
|
||||||
|
|
||||||
|
# PM10 (single-unit: μg/m³)
|
||||||
|
pm10_changed:
|
||||||
|
target: *target_pm10
|
||||||
|
fields:
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *pm10_threshold_entity
|
||||||
|
mode: changed
|
||||||
|
number: *ugm3_threshold_number
|
||||||
|
|
||||||
|
pm10_crossed_threshold:
|
||||||
|
target: *target_pm10
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *pm10_threshold_entity
|
||||||
|
mode: crossed
|
||||||
|
number: *ugm3_threshold_number
|
||||||
|
|
||||||
|
# Ozone (multi-unit)
|
||||||
|
ozone_changed:
|
||||||
|
target: *target_ozone
|
||||||
|
fields:
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *ozone_threshold_entity
|
||||||
|
mode: changed
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *ozone_units
|
||||||
|
|
||||||
|
ozone_crossed_threshold:
|
||||||
|
target: *target_ozone
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *ozone_threshold_entity
|
||||||
|
mode: crossed
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *ozone_units
|
||||||
|
|
||||||
|
# VOC (multi-unit)
|
||||||
|
voc_changed:
|
||||||
|
target: *target_voc
|
||||||
|
fields:
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *voc_threshold_entity
|
||||||
|
mode: changed
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *voc_units
|
||||||
|
|
||||||
|
voc_crossed_threshold:
|
||||||
|
target: *target_voc
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *voc_threshold_entity
|
||||||
|
mode: crossed
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *voc_units
|
||||||
|
|
||||||
|
# VOC ratio (multi-unit)
|
||||||
|
voc_ratio_changed:
|
||||||
|
target: *target_voc_ratio
|
||||||
|
fields:
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *voc_ratio_threshold_entity
|
||||||
|
mode: changed
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *voc_ratio_units
|
||||||
|
|
||||||
|
voc_ratio_crossed_threshold:
|
||||||
|
target: *target_voc_ratio
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *voc_ratio_threshold_entity
|
||||||
|
mode: crossed
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *voc_ratio_units
|
||||||
|
|
||||||
|
# NO (multi-unit)
|
||||||
|
no_changed:
|
||||||
|
target: *target_no
|
||||||
|
fields:
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *no_threshold_entity
|
||||||
|
mode: changed
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *no_units
|
||||||
|
|
||||||
|
no_crossed_threshold:
|
||||||
|
target: *target_no
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *no_threshold_entity
|
||||||
|
mode: crossed
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *no_units
|
||||||
|
|
||||||
|
# NO2 (multi-unit)
|
||||||
|
no2_changed:
|
||||||
|
target: *target_no2
|
||||||
|
fields:
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *no2_threshold_entity
|
||||||
|
mode: changed
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *no2_units
|
||||||
|
|
||||||
|
no2_crossed_threshold:
|
||||||
|
target: *target_no2
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *no2_threshold_entity
|
||||||
|
mode: crossed
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *no2_units
|
||||||
|
|
||||||
|
# N2O (single-unit: μg/m³)
|
||||||
|
n2o_changed:
|
||||||
|
target: *target_n2o
|
||||||
|
fields:
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *n2o_threshold_entity
|
||||||
|
mode: changed
|
||||||
|
number: *ugm3_threshold_number
|
||||||
|
|
||||||
|
n2o_crossed_threshold:
|
||||||
|
target: *target_n2o
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *n2o_threshold_entity
|
||||||
|
mode: crossed
|
||||||
|
number: *ugm3_threshold_number
|
||||||
|
|
||||||
|
# SO2 (multi-unit)
|
||||||
|
so2_changed:
|
||||||
|
target: *target_so2
|
||||||
|
fields:
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *so2_threshold_entity
|
||||||
|
mode: changed
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *so2_units
|
||||||
|
|
||||||
|
so2_crossed_threshold:
|
||||||
|
target: *target_so2
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *so2_threshold_entity
|
||||||
|
mode: crossed
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: *so2_units
|
||||||
@@ -18,6 +18,10 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
|||||||
SchemaOptionsFlowHandler,
|
SchemaOptionsFlowHandler,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.selector import BooleanSelector
|
from homeassistant.helpers.selector import BooleanSelector
|
||||||
|
from homeassistant.helpers.service_info.zeroconf import (
|
||||||
|
ATTR_PROPERTIES_ID,
|
||||||
|
ZeroconfServiceInfo,
|
||||||
|
)
|
||||||
|
|
||||||
from .const import CONF_CLIP_NEGATIVE, CONF_RETURN_AVERAGE, DOMAIN
|
from .const import CONF_CLIP_NEGATIVE, CONF_RETURN_AVERAGE, DOMAIN
|
||||||
|
|
||||||
@@ -46,6 +50,9 @@ class AirQConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
|
_discovered_host: str
|
||||||
|
_discovered_name: str
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
@@ -80,7 +87,7 @@ class AirQConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
_LOGGER.debug("Successfully connected to %s", user_input[CONF_IP_ADDRESS])
|
_LOGGER.debug("Successfully connected to %s", user_input[CONF_IP_ADDRESS])
|
||||||
|
|
||||||
device_info = await airq.fetch_device_info()
|
device_info = await airq.fetch_device_info()
|
||||||
await self.async_set_unique_id(device_info["id"])
|
await self.async_set_unique_id(device_info["id"], raise_on_progress=False)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
_LOGGER.debug("Creating an entry for %s", device_info["name"])
|
_LOGGER.debug("Creating an entry for %s", device_info["name"])
|
||||||
@@ -90,6 +97,58 @@ class AirQConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_zeroconf(
|
||||||
|
self, discovery_info: ZeroconfServiceInfo
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle zeroconf discovery of an air-Q device."""
|
||||||
|
self._discovered_host = discovery_info.host
|
||||||
|
self._discovered_name = discovery_info.properties.get("devicename", "air-Q")
|
||||||
|
device_id = discovery_info.properties.get(ATTR_PROPERTIES_ID)
|
||||||
|
|
||||||
|
if not device_id:
|
||||||
|
return self.async_abort(reason="incomplete_discovery")
|
||||||
|
|
||||||
|
await self.async_set_unique_id(device_id)
|
||||||
|
self._abort_if_unique_id_configured(
|
||||||
|
updates={CONF_IP_ADDRESS: self._discovered_host},
|
||||||
|
reload_on_update=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.context["title_placeholders"] = {"name": self._discovered_name}
|
||||||
|
|
||||||
|
return await self.async_step_discovery_confirm()
|
||||||
|
|
||||||
|
async def async_step_discovery_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle user confirmation of a discovered air-Q device."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
session = async_get_clientsession(self.hass)
|
||||||
|
airq = AirQ(self._discovered_host, user_input[CONF_PASSWORD], session)
|
||||||
|
try:
|
||||||
|
await airq.validate()
|
||||||
|
except ClientConnectionError:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
else:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self._discovered_name,
|
||||||
|
data={
|
||||||
|
CONF_IP_ADDRESS: self._discovered_host,
|
||||||
|
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="discovery_confirm",
|
||||||
|
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
|
||||||
|
description_placeholders={"name": self._discovered_name},
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""Diagnostics support for air-Q."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
|
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_UNIQUE_ID
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import AirQConfigEntry
|
||||||
|
|
||||||
|
REDACT_CONFIG = {CONF_PASSWORD, CONF_UNIQUE_ID, CONF_IP_ADDRESS, "title"}
|
||||||
|
REDACT_DEVICE_INFO = {"identifiers", "name"}
|
||||||
|
REDACT_COORDINATOR_DATA = {"DeviceID"}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_config_entry_diagnostics(
|
||||||
|
hass: HomeAssistant, entry: AirQConfigEntry
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return diagnostics for a config entry."""
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
|
return {
|
||||||
|
"config_entry": async_redact_data(entry.as_dict(), REDACT_CONFIG),
|
||||||
|
"device_info": async_redact_data(
|
||||||
|
dict(coordinator.device_info), REDACT_DEVICE_INFO
|
||||||
|
),
|
||||||
|
"coordinator_data": async_redact_data(
|
||||||
|
coordinator.data, REDACT_COORDINATOR_DATA
|
||||||
|
),
|
||||||
|
"options": {
|
||||||
|
"clip_negative": coordinator.clip_negative,
|
||||||
|
"return_average": coordinator.return_average,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -7,5 +7,13 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioairq"],
|
"loggers": ["aioairq"],
|
||||||
"requirements": ["aioairq==0.4.7"]
|
"requirements": ["aioairq==0.4.7"],
|
||||||
|
"zeroconf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"device": "air-q"
|
||||||
|
},
|
||||||
|
"type": "_http._tcp.local."
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
|
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||||
|
"incomplete_discovery": "The discovered air-Q device did not provide a device ID. Ensure the firmware is up to date."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
"invalid_input": "[%key:common::config_flow::error::invalid_host%]"
|
"invalid_input": "[%key:common::config_flow::error::invalid_host%]"
|
||||||
},
|
},
|
||||||
|
"flow_title": "{name}",
|
||||||
"step": {
|
"step": {
|
||||||
|
"discovery_confirm": {
|
||||||
|
"data": {
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
},
|
||||||
|
"description": "Do you want to set up **{name}**?",
|
||||||
|
"title": "Set up air-Q"
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"ip_address": "[%key:common::config_flow::data::ip%]",
|
"ip_address": "[%key:common::config_flow::data::ip%]",
|
||||||
|
|||||||
@@ -117,23 +117,23 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
|
|||||||
return super()._handle_coordinator_update()
|
return super()._handle_coordinator_update()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_temperature(self):
|
def current_temperature(self) -> int:
|
||||||
"""Return the current temperature."""
|
"""Return the current temperature."""
|
||||||
return self._unit.Temperature
|
return self._unit.Temperature
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_mode(self):
|
def fan_mode(self) -> str:
|
||||||
"""Return fan mode of the AC this group belongs to."""
|
"""Return fan mode of the AC this group belongs to."""
|
||||||
return AT_TO_HA_FAN_SPEED[self._airtouch.acs[self._ac_number].AcFanSpeed]
|
return AT_TO_HA_FAN_SPEED[self._airtouch.acs[self._ac_number].AcFanSpeed]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_modes(self):
|
def fan_modes(self) -> list[str]:
|
||||||
"""Return the list of available fan modes."""
|
"""Return the list of available fan modes."""
|
||||||
airtouch_fan_speeds = self._airtouch.GetSupportedFanSpeedsForAc(self._ac_number)
|
airtouch_fan_speeds = self._airtouch.GetSupportedFanSpeedsForAc(self._ac_number)
|
||||||
return [AT_TO_HA_FAN_SPEED[speed] for speed in airtouch_fan_speeds]
|
return [AT_TO_HA_FAN_SPEED[speed] for speed in airtouch_fan_speeds]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_mode(self):
|
def hvac_mode(self) -> HVACMode:
|
||||||
"""Return hvac target hvac state."""
|
"""Return hvac target hvac state."""
|
||||||
is_off = self._unit.PowerState == "Off"
|
is_off = self._unit.PowerState == "Off"
|
||||||
if is_off:
|
if is_off:
|
||||||
@@ -236,17 +236,17 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
|
|||||||
return self._airtouch.acs[self._unit.BelongsToAc].MaxSetpoint
|
return self._airtouch.acs[self._unit.BelongsToAc].MaxSetpoint
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_temperature(self):
|
def current_temperature(self) -> int:
|
||||||
"""Return the current temperature."""
|
"""Return the current temperature."""
|
||||||
return self._unit.Temperature
|
return self._unit.Temperature
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature(self):
|
def target_temperature(self) -> int:
|
||||||
"""Return the temperature we are trying to reach."""
|
"""Return the temperature we are trying to reach."""
|
||||||
return self._unit.TargetSetpoint
|
return self._unit.TargetSetpoint
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_mode(self):
|
def hvac_mode(self) -> HVACMode:
|
||||||
"""Return hvac target hvac state."""
|
"""Return hvac target hvac state."""
|
||||||
# there are other power states that aren't 'on' but still count as on (eg. 'Turbo')
|
# there are other power states that aren't 'on' but still count as on (eg. 'Turbo')
|
||||||
is_off = self._unit.PowerState == "Off"
|
is_off = self._unit.PowerState == "Off"
|
||||||
@@ -272,12 +272,12 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_mode(self):
|
def fan_mode(self) -> str:
|
||||||
"""Return fan mode of the AC this group belongs to."""
|
"""Return fan mode of the AC this group belongs to."""
|
||||||
return AT_TO_HA_FAN_SPEED[self._airtouch.acs[self._unit.BelongsToAc].AcFanSpeed]
|
return AT_TO_HA_FAN_SPEED[self._airtouch.acs[self._unit.BelongsToAc].AcFanSpeed]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_modes(self):
|
def fan_modes(self) -> list[str]:
|
||||||
"""Return the list of available fan modes."""
|
"""Return the list of available fan modes."""
|
||||||
airtouch_fan_speeds = self._airtouch.GetSupportedFanSpeedsByGroup(
|
airtouch_fan_speeds = self._airtouch.GetSupportedFanSpeedsByGroup(
|
||||||
self._group_number
|
self._group_number
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["airtouch5py"],
|
"loggers": ["airtouch5py"],
|
||||||
"requirements": ["airtouch5py==0.3.0"]
|
"requirements": ["airtouch5py==0.4.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,7 @@ from datetime import timedelta
|
|||||||
from math import ceil
|
from math import ceil
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pyairvisual.cloud_api import (
|
from pyairvisual.cloud_api import CloudAPI
|
||||||
CloudAPI,
|
|
||||||
InvalidKeyError,
|
|
||||||
KeyExpiredError,
|
|
||||||
UnauthorizedError,
|
|
||||||
)
|
|
||||||
from pyairvisual.errors import AirVisualError
|
|
||||||
|
|
||||||
from homeassistant.components import automation
|
from homeassistant.components import automation
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
@@ -28,14 +22,12 @@ from homeassistant.const import (
|
|||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
aiohttp_client,
|
aiohttp_client,
|
||||||
device_registry as dr,
|
device_registry as dr,
|
||||||
entity_registry as er,
|
entity_registry as er,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_CITY,
|
CONF_CITY,
|
||||||
@@ -47,8 +39,7 @@ from .const import (
|
|||||||
INTEGRATION_TYPE_NODE_PRO,
|
INTEGRATION_TYPE_NODE_PRO,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
)
|
)
|
||||||
|
from .coordinator import AirVisualConfigEntry, AirVisualDataUpdateCoordinator
|
||||||
type AirVisualConfigEntry = ConfigEntry[DataUpdateCoordinator]
|
|
||||||
|
|
||||||
# We use a raw string for the airvisual_pro domain (instead of importing the actual
|
# We use a raw string for the airvisual_pro domain (instead of importing the actual
|
||||||
# constant) so that we can avoid listing it as a dependency:
|
# constant) so that we can avoid listing it as a dependency:
|
||||||
@@ -85,8 +76,8 @@ def async_get_cloud_api_update_interval(
|
|||||||
@callback
|
@callback
|
||||||
def async_get_cloud_coordinators_by_api_key(
|
def async_get_cloud_coordinators_by_api_key(
|
||||||
hass: HomeAssistant, api_key: str
|
hass: HomeAssistant, api_key: str
|
||||||
) -> list[DataUpdateCoordinator]:
|
) -> list[AirVisualDataUpdateCoordinator]:
|
||||||
"""Get all DataUpdateCoordinator objects related to a particular API key."""
|
"""Get all AirVisualDataUpdateCoordinator objects related to a particular API key."""
|
||||||
return [
|
return [
|
||||||
entry.runtime_data
|
entry.runtime_data
|
||||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||||
@@ -180,38 +171,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirVisualConfigEntry) ->
|
|||||||
websession = aiohttp_client.async_get_clientsession(hass)
|
websession = aiohttp_client.async_get_clientsession(hass)
|
||||||
cloud_api = CloudAPI(entry.data[CONF_API_KEY], session=websession)
|
cloud_api = CloudAPI(entry.data[CONF_API_KEY], session=websession)
|
||||||
|
|
||||||
async def async_update_data() -> dict[str, Any]:
|
coordinator = AirVisualDataUpdateCoordinator(
|
||||||
"""Get new data from the API."""
|
|
||||||
if CONF_CITY in entry.data:
|
|
||||||
api_coro = cloud_api.air_quality.city(
|
|
||||||
entry.data[CONF_CITY],
|
|
||||||
entry.data[CONF_STATE],
|
|
||||||
entry.data[CONF_COUNTRY],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
api_coro = cloud_api.air_quality.nearest_city(
|
|
||||||
entry.data[CONF_LATITUDE],
|
|
||||||
entry.data[CONF_LONGITUDE],
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return await api_coro
|
|
||||||
except (InvalidKeyError, KeyExpiredError, UnauthorizedError) as ex:
|
|
||||||
raise ConfigEntryAuthFailed from ex
|
|
||||||
except AirVisualError as err:
|
|
||||||
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
|
||||||
|
|
||||||
coordinator = DataUpdateCoordinator(
|
|
||||||
hass,
|
hass,
|
||||||
LOGGER,
|
entry,
|
||||||
config_entry=entry,
|
cloud_api,
|
||||||
name=async_get_geography_id(entry.data),
|
name=async_get_geography_id(entry.data),
|
||||||
# We give a placeholder update interval in order to create the coordinator;
|
|
||||||
# then, below, we use the coordinator's presence (along with any other
|
|
||||||
# coordinators using the same API key) to calculate an actual, leveled
|
|
||||||
# update interval:
|
|
||||||
update_interval=timedelta(minutes=5),
|
|
||||||
update_method=async_update_data,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
|
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""Define an AirVisual data coordinator."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pyairvisual.cloud_api import (
|
||||||
|
CloudAPI,
|
||||||
|
InvalidKeyError,
|
||||||
|
KeyExpiredError,
|
||||||
|
UnauthorizedError,
|
||||||
|
)
|
||||||
|
from pyairvisual.errors import AirVisualError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_COUNTRY, CONF_LATITUDE, CONF_LONGITUDE, CONF_STATE
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import CONF_CITY, LOGGER
|
||||||
|
|
||||||
|
type AirVisualConfigEntry = ConfigEntry[AirVisualDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
class AirVisualDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
|
"""Class to manage fetching AirVisual data."""
|
||||||
|
|
||||||
|
config_entry: AirVisualConfigEntry
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: AirVisualConfigEntry,
|
||||||
|
cloud_api: CloudAPI,
|
||||||
|
name: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the coordinator."""
|
||||||
|
self._cloud_api = cloud_api
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
|
name=name,
|
||||||
|
# We give a placeholder update interval in order to create the coordinator;
|
||||||
|
# then, in async_setup_entry, we use the coordinator's presence (along with
|
||||||
|
# any other coordinators using the same API key) to calculate an actual,
|
||||||
|
# leveled update interval:
|
||||||
|
update_interval=timedelta(minutes=5),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, Any]:
|
||||||
|
"""Get new data from the API."""
|
||||||
|
if CONF_CITY in self.config_entry.data:
|
||||||
|
api_coro = self._cloud_api.air_quality.city(
|
||||||
|
self.config_entry.data[CONF_CITY],
|
||||||
|
self.config_entry.data[CONF_STATE],
|
||||||
|
self.config_entry.data[CONF_COUNTRY],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
api_coro = self._cloud_api.air_quality.nearest_city(
|
||||||
|
self.config_entry.data[CONF_LATITUDE],
|
||||||
|
self.config_entry.data[CONF_LONGITUDE],
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await api_coro
|
||||||
|
except (InvalidKeyError, KeyExpiredError, UnauthorizedError) as ex:
|
||||||
|
raise ConfigEntryAuthFailed from ex
|
||||||
|
except AirVisualError as err:
|
||||||
|
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
||||||
@@ -15,8 +15,8 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from . import AirVisualConfigEntry
|
|
||||||
from .const import CONF_CITY
|
from .const import CONF_CITY
|
||||||
|
from .coordinator import AirVisualConfigEntry
|
||||||
|
|
||||||
CONF_COORDINATES = "coordinates"
|
CONF_COORDINATES = "coordinates"
|
||||||
CONF_TITLE = "title"
|
CONF_TITLE = "title"
|
||||||
|
|||||||
@@ -2,29 +2,25 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
CoordinatorEntity,
|
|
||||||
DataUpdateCoordinator,
|
from .coordinator import AirVisualDataUpdateCoordinator
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AirVisualEntity(CoordinatorEntity):
|
class AirVisualEntity(CoordinatorEntity[AirVisualDataUpdateCoordinator]):
|
||||||
"""Define a generic AirVisual entity."""
|
"""Define a generic AirVisual entity."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: DataUpdateCoordinator,
|
coordinator: AirVisualDataUpdateCoordinator,
|
||||||
entry: ConfigEntry,
|
|
||||||
description: EntityDescription,
|
description: EntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
self._attr_extra_state_attributes = {}
|
self._attr_extra_state_attributes = {}
|
||||||
self._entry = entry
|
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_LATITUDE,
|
ATTR_LATITUDE,
|
||||||
ATTR_LONGITUDE,
|
ATTR_LONGITUDE,
|
||||||
@@ -24,10 +23,9 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
||||||
|
|
||||||
from . import AirVisualConfigEntry
|
|
||||||
from .const import CONF_CITY
|
from .const import CONF_CITY
|
||||||
|
from .coordinator import AirVisualConfigEntry, AirVisualDataUpdateCoordinator
|
||||||
from .entity import AirVisualEntity
|
from .entity import AirVisualEntity
|
||||||
|
|
||||||
ATTR_CITY = "city"
|
ATTR_CITY = "city"
|
||||||
@@ -113,7 +111,7 @@ async def async_setup_entry(
|
|||||||
"""Set up AirVisual sensors based on a config entry."""
|
"""Set up AirVisual sensors based on a config entry."""
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
AirVisualGeographySensor(coordinator, entry, description, locale)
|
AirVisualGeographySensor(coordinator, description, locale)
|
||||||
for locale in GEOGRAPHY_SENSOR_LOCALES
|
for locale in GEOGRAPHY_SENSOR_LOCALES
|
||||||
for description in GEOGRAPHY_SENSOR_DESCRIPTIONS
|
for description in GEOGRAPHY_SENSOR_DESCRIPTIONS
|
||||||
)
|
)
|
||||||
@@ -124,14 +122,14 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: DataUpdateCoordinator,
|
coordinator: AirVisualDataUpdateCoordinator,
|
||||||
entry: ConfigEntry,
|
|
||||||
description: SensorEntityDescription,
|
description: SensorEntityDescription,
|
||||||
locale: str,
|
locale: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
super().__init__(coordinator, entry, description)
|
super().__init__(coordinator, description)
|
||||||
|
|
||||||
|
entry = coordinator.config_entry
|
||||||
self._attr_extra_state_attributes.update(
|
self._attr_extra_state_attributes.update(
|
||||||
{
|
{
|
||||||
ATTR_CITY: entry.data.get(CONF_CITY),
|
ATTR_CITY: entry.data.get(CONF_CITY),
|
||||||
@@ -182,16 +180,16 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
|
|||||||
#
|
#
|
||||||
# We use any coordinates in the config entry and, in the case of a geography by
|
# We use any coordinates in the config entry and, in the case of a geography by
|
||||||
# name, we fall back to the latitude longitude provided in the coordinator data:
|
# name, we fall back to the latitude longitude provided in the coordinator data:
|
||||||
latitude = self._entry.data.get(
|
latitude = self.coordinator.config_entry.data.get(
|
||||||
CONF_LATITUDE,
|
CONF_LATITUDE,
|
||||||
self.coordinator.data["location"]["coordinates"][1],
|
self.coordinator.data["location"]["coordinates"][1],
|
||||||
)
|
)
|
||||||
longitude = self._entry.data.get(
|
longitude = self.coordinator.config_entry.data.get(
|
||||||
CONF_LONGITUDE,
|
CONF_LONGITUDE,
|
||||||
self.coordinator.data["location"]["coordinates"][0],
|
self.coordinator.data["location"]["coordinates"][0],
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._entry.options[CONF_SHOW_ON_MAP]:
|
if self.coordinator.config_entry.options[CONF_SHOW_ON_MAP]:
|
||||||
self._attr_extra_state_attributes[ATTR_LATITUDE] = latitude
|
self._attr_extra_state_attributes[ATTR_LATITUDE] = latitude
|
||||||
self._attr_extra_state_attributes[ATTR_LONGITUDE] = longitude
|
self._attr_extra_state_attributes[ATTR_LONGITUDE] = longitude
|
||||||
self._attr_extra_state_attributes.pop("lati", None)
|
self._attr_extra_state_attributes.pop("lati", None)
|
||||||
|
|||||||
@@ -4,18 +4,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import timedelta
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from pyairvisual.node import (
|
from pyairvisual.node import NodeProError, NodeSamba
|
||||||
InvalidAuthenticationError,
|
|
||||||
NodeConnectionError,
|
|
||||||
NodeProError,
|
|
||||||
NodeSamba,
|
|
||||||
)
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_IP_ADDRESS,
|
CONF_IP_ADDRESS,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
@@ -23,25 +14,16 @@ from homeassistant.const import (
|
|||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
from homeassistant.core import Event, HomeAssistant
|
from homeassistant.core import Event, HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
|
||||||
|
|
||||||
from .const import LOGGER
|
from .coordinator import (
|
||||||
|
AirVisualProConfigEntry,
|
||||||
|
AirVisualProCoordinator,
|
||||||
|
AirVisualProData,
|
||||||
|
)
|
||||||
|
|
||||||
PLATFORMS = [Platform.SENSOR]
|
PLATFORMS = [Platform.SENSOR]
|
||||||
|
|
||||||
UPDATE_INTERVAL = timedelta(minutes=1)
|
|
||||||
|
|
||||||
type AirVisualProConfigEntry = ConfigEntry[AirVisualProData]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AirVisualProData:
|
|
||||||
"""Define a data class."""
|
|
||||||
|
|
||||||
coordinator: DataUpdateCoordinator
|
|
||||||
node: NodeSamba
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: AirVisualProConfigEntry
|
hass: HomeAssistant, entry: AirVisualProConfigEntry
|
||||||
@@ -54,48 +36,15 @@ async def async_setup_entry(
|
|||||||
except NodeProError as err:
|
except NodeProError as err:
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
reload_task: asyncio.Task | None = None
|
coordinator = AirVisualProCoordinator(hass, entry, node)
|
||||||
|
|
||||||
async def async_get_data() -> dict[str, Any]:
|
|
||||||
"""Get data from the device."""
|
|
||||||
try:
|
|
||||||
data = await node.async_get_latest_measurements()
|
|
||||||
data["history"] = {}
|
|
||||||
if data["settings"].get("follow_mode") == "device":
|
|
||||||
history = await node.async_get_history(include_trends=False)
|
|
||||||
data["history"] = history.get("measurements", [])[-1]
|
|
||||||
except InvalidAuthenticationError as err:
|
|
||||||
raise ConfigEntryAuthFailed("Invalid Samba password") from err
|
|
||||||
except NodeConnectionError as err:
|
|
||||||
nonlocal reload_task
|
|
||||||
if not reload_task:
|
|
||||||
reload_task = hass.async_create_task(
|
|
||||||
hass.config_entries.async_reload(entry.entry_id)
|
|
||||||
)
|
|
||||||
raise UpdateFailed(f"Connection to Pro unit lost: {err}") from err
|
|
||||||
except NodeProError as err:
|
|
||||||
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
coordinator = DataUpdateCoordinator(
|
|
||||||
hass,
|
|
||||||
LOGGER,
|
|
||||||
config_entry=entry,
|
|
||||||
name="Node/Pro data",
|
|
||||||
update_interval=UPDATE_INTERVAL,
|
|
||||||
update_method=async_get_data,
|
|
||||||
)
|
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
entry.runtime_data = AirVisualProData(coordinator=coordinator, node=node)
|
entry.runtime_data = AirVisualProData(coordinator=coordinator, node=node)
|
||||||
|
|
||||||
async def async_shutdown(_: Event) -> None:
|
async def async_shutdown(_: Event) -> None:
|
||||||
"""Define an event handler to disconnect from the websocket."""
|
"""Define an event handler to disconnect from the websocket."""
|
||||||
nonlocal reload_task
|
if coordinator.reload_task:
|
||||||
if reload_task:
|
|
||||||
with suppress(asyncio.CancelledError):
|
with suppress(asyncio.CancelledError):
|
||||||
reload_task.cancel()
|
coordinator.reload_task.cancel()
|
||||||
await node.async_disconnect()
|
await node.async_disconnect()
|
||||||
|
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"""DataUpdateCoordinator for the AirVisual Pro integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pyairvisual.node import (
|
||||||
|
InvalidAuthenticationError,
|
||||||
|
NodeConnectionError,
|
||||||
|
NodeProError,
|
||||||
|
NodeSamba,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import LOGGER
|
||||||
|
|
||||||
|
UPDATE_INTERVAL = timedelta(minutes=1)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AirVisualProData:
|
||||||
|
"""Define a data class."""
|
||||||
|
|
||||||
|
coordinator: AirVisualProCoordinator
|
||||||
|
node: NodeSamba
|
||||||
|
|
||||||
|
|
||||||
|
type AirVisualProConfigEntry = ConfigEntry[AirVisualProData]
|
||||||
|
|
||||||
|
|
||||||
|
class AirVisualProCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
|
"""Coordinator for AirVisual Pro data."""
|
||||||
|
|
||||||
|
config_entry: AirVisualProConfigEntry
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: AirVisualProConfigEntry,
|
||||||
|
node: NodeSamba,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
|
name="Node/Pro data",
|
||||||
|
update_interval=UPDATE_INTERVAL,
|
||||||
|
)
|
||||||
|
self._node = node
|
||||||
|
self.reload_task: asyncio.Task[bool] | None = None
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, Any]:
|
||||||
|
"""Get data from the device."""
|
||||||
|
try:
|
||||||
|
data = await self._node.async_get_latest_measurements()
|
||||||
|
data["history"] = {}
|
||||||
|
if data["settings"].get("follow_mode") == "device":
|
||||||
|
history = await self._node.async_get_history(include_trends=False)
|
||||||
|
data["history"] = history.get("measurements", [])[-1]
|
||||||
|
except InvalidAuthenticationError as err:
|
||||||
|
raise ConfigEntryAuthFailed("Invalid Samba password") from err
|
||||||
|
except NodeConnectionError as err:
|
||||||
|
if self.reload_task is None:
|
||||||
|
self.reload_task = self.hass.async_create_task(
|
||||||
|
self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
||||||
|
)
|
||||||
|
raise UpdateFailed(f"Connection to Pro unit lost: {err}") from err
|
||||||
|
except NodeProError as err:
|
||||||
|
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
||||||
|
|
||||||
|
return data
|
||||||
@@ -8,7 +8,7 @@ from homeassistant.components.diagnostics import async_redact_data
|
|||||||
from homeassistant.const import CONF_PASSWORD
|
from homeassistant.const import CONF_PASSWORD
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from . import AirVisualProConfigEntry
|
from .coordinator import AirVisualProConfigEntry
|
||||||
|
|
||||||
CONF_MAC_ADDRESS = "mac_address"
|
CONF_MAC_ADDRESS = "mac_address"
|
||||||
CONF_SERIAL_NUMBER = "serial_number"
|
CONF_SERIAL_NUMBER = "serial_number"
|
||||||
|
|||||||
@@ -4,19 +4,17 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
CoordinatorEntity,
|
|
||||||
DataUpdateCoordinator,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AirVisualProCoordinator
|
||||||
|
|
||||||
|
|
||||||
class AirVisualProEntity(CoordinatorEntity):
|
class AirVisualProEntity(CoordinatorEntity[AirVisualProCoordinator]):
|
||||||
"""Define a generic AirVisual Pro entity."""
|
"""Define a generic AirVisual Pro entity."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, coordinator: DataUpdateCoordinator, description: EntityDescription
|
self, coordinator: AirVisualProCoordinator, description: EntityDescription
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import AirVisualProConfigEntry
|
from .coordinator import AirVisualProConfigEntry
|
||||||
from .entity import AirVisualProEntity
|
from .entity import AirVisualProEntity
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ from homeassistant.helpers import (
|
|||||||
config_entry_oauth2_flow,
|
config_entry_oauth2_flow,
|
||||||
device_registry as dr,
|
device_registry as dr,
|
||||||
)
|
)
|
||||||
|
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||||
|
ImplementationUnavailableError,
|
||||||
|
)
|
||||||
|
|
||||||
from . import api
|
from . import api
|
||||||
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN
|
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN
|
||||||
@@ -25,11 +28,17 @@ async def async_setup_entry(
|
|||||||
hass: HomeAssistant, entry: AladdinConnectConfigEntry
|
hass: HomeAssistant, entry: AladdinConnectConfigEntry
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Set up Aladdin Connect Genie from a config entry."""
|
"""Set up Aladdin Connect Genie from a config entry."""
|
||||||
implementation = (
|
try:
|
||||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
implementation = (
|
||||||
hass, entry
|
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||||
|
hass, entry
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
except ImplementationUnavailableError as err:
|
||||||
|
raise ConfigEntryNotReady(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="oauth2_implementation_unavailable",
|
||||||
|
) from err
|
||||||
|
|
||||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||||
|
|
||||||
@@ -46,19 +55,10 @@ async def async_setup_entry(
|
|||||||
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
|
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
coordinator = AladdinConnectCoordinator(hass, entry, client)
|
||||||
doors = await client.get_doors()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
except aiohttp.ClientResponseError as err:
|
|
||||||
if 400 <= err.status < 500:
|
|
||||||
raise ConfigEntryAuthFailed(err) from err
|
|
||||||
raise ConfigEntryNotReady from err
|
|
||||||
except aiohttp.ClientError as err:
|
|
||||||
raise ConfigEntryNotReady from err
|
|
||||||
|
|
||||||
entry.runtime_data = {
|
entry.runtime_data = coordinator
|
||||||
door.unique_id: AladdinConnectCoordinator(hass, entry, client, door)
|
|
||||||
for door in doors
|
|
||||||
}
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ def remove_stale_devices(
|
|||||||
device_entries = dr.async_entries_for_config_entry(
|
device_entries = dr.async_entries_for_config_entry(
|
||||||
device_registry, config_entry.entry_id
|
device_registry, config_entry.entry_id
|
||||||
)
|
)
|
||||||
all_device_ids = set(config_entry.runtime_data)
|
all_device_ids = set(config_entry.runtime_data.data)
|
||||||
|
|
||||||
for device_entry in device_entries:
|
for device_entry in device_entries:
|
||||||
device_id: str | None = None
|
device_id: str | None = None
|
||||||
|
|||||||
@@ -11,22 +11,24 @@ from genie_partner_sdk.model import GarageDoor
|
|||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
type AladdinConnectConfigEntry = ConfigEntry[dict[str, AladdinConnectCoordinator]]
|
type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator]
|
||||||
SCAN_INTERVAL = timedelta(seconds=15)
|
SCAN_INTERVAL = timedelta(seconds=15)
|
||||||
|
|
||||||
|
|
||||||
class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
|
class AladdinConnectCoordinator(DataUpdateCoordinator[dict[str, GarageDoor]]):
|
||||||
"""Coordinator for Aladdin Connect integration."""
|
"""Coordinator for Aladdin Connect integration."""
|
||||||
|
|
||||||
|
config_entry: AladdinConnectConfigEntry
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: AladdinConnectConfigEntry,
|
entry: AladdinConnectConfigEntry,
|
||||||
client: AladdinConnectClient,
|
client: AladdinConnectClient,
|
||||||
garage_door: GarageDoor,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the coordinator."""
|
"""Initialize the coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -37,18 +39,16 @@ class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
|
|||||||
update_interval=SCAN_INTERVAL,
|
update_interval=SCAN_INTERVAL,
|
||||||
)
|
)
|
||||||
self.client = client
|
self.client = client
|
||||||
self.data = garage_door
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> GarageDoor:
|
async def _async_update_data(self) -> dict[str, GarageDoor]:
|
||||||
"""Fetch data from the Aladdin Connect API."""
|
"""Fetch data from the Aladdin Connect API."""
|
||||||
try:
|
try:
|
||||||
await self.client.update_door(self.data.device_id, self.data.door_number)
|
doors = await self.client.get_doors()
|
||||||
|
except aiohttp.ClientResponseError as err:
|
||||||
|
if 400 <= err.status < 500:
|
||||||
|
raise ConfigEntryAuthFailed(err) from err
|
||||||
|
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||||
except aiohttp.ClientError as err:
|
except aiohttp.ClientError as err:
|
||||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||||
self.data.status = self.client.get_door_status(
|
|
||||||
self.data.device_id, self.data.door_number
|
return {door.unique_id: door for door in doors}
|
||||||
)
|
|
||||||
self.data.battery_level = self.client.get_battery_status(
|
|
||||||
self.data.device_id, self.data.door_number
|
|
||||||
)
|
|
||||||
return self.data
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from typing import Any
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
|
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
@@ -24,11 +24,22 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the cover platform."""
|
"""Set up the cover platform."""
|
||||||
coordinators = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
|
known_devices: set[str] = set()
|
||||||
|
|
||||||
async_add_entities(
|
@callback
|
||||||
AladdinCoverEntity(coordinator) for coordinator in coordinators.values()
|
def _async_add_new_devices() -> None:
|
||||||
)
|
"""Detect and add entities for new doors."""
|
||||||
|
current_devices = set(coordinator.data)
|
||||||
|
new_devices = current_devices - known_devices
|
||||||
|
if new_devices:
|
||||||
|
known_devices.update(new_devices)
|
||||||
|
async_add_entities(
|
||||||
|
AladdinCoverEntity(coordinator, door_id) for door_id in new_devices
|
||||||
|
)
|
||||||
|
|
||||||
|
_async_add_new_devices()
|
||||||
|
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
|
||||||
|
|
||||||
|
|
||||||
class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
|
class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
|
||||||
@@ -38,10 +49,10 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
|
|||||||
_attr_supported_features = SUPPORTED_FEATURES
|
_attr_supported_features = SUPPORTED_FEATURES
|
||||||
_attr_name = None
|
_attr_name = None
|
||||||
|
|
||||||
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
|
def __init__(self, coordinator: AladdinConnectCoordinator, door_id: str) -> None:
|
||||||
"""Initialize the Aladdin Connect cover."""
|
"""Initialize the Aladdin Connect cover."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator, door_id)
|
||||||
self._attr_unique_id = coordinator.data.unique_id
|
self._attr_unique_id = door_id
|
||||||
|
|
||||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||||
"""Issue open command to cover."""
|
"""Issue open command to cover."""
|
||||||
@@ -66,16 +77,16 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
|
|||||||
@property
|
@property
|
||||||
def is_closed(self) -> bool | None:
|
def is_closed(self) -> bool | None:
|
||||||
"""Update is closed attribute."""
|
"""Update is closed attribute."""
|
||||||
if (status := self.coordinator.data.status) is None:
|
if (status := self.door.status) is None:
|
||||||
return None
|
return None
|
||||||
return status == "closed"
|
return status == "closed"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_closing(self) -> bool | None:
|
def is_closing(self) -> bool | None:
|
||||||
"""Update is closing attribute."""
|
"""Update is closing attribute."""
|
||||||
return self.coordinator.data.status == "closing"
|
return self.door.status == "closing"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_opening(self) -> bool | None:
|
def is_opening(self) -> bool | None:
|
||||||
"""Update is opening attribute."""
|
"""Update is opening attribute."""
|
||||||
return self.coordinator.data.status == "opening"
|
return self.door.status == "opening"
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ async def async_get_config_entry_diagnostics(
|
|||||||
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
|
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
|
||||||
"doors": {
|
"doors": {
|
||||||
uid: {
|
uid: {
|
||||||
"device_id": coordinator.data.device_id,
|
"device_id": door.device_id,
|
||||||
"door_number": coordinator.data.door_number,
|
"door_number": door.door_number,
|
||||||
"name": coordinator.data.name,
|
"name": door.name,
|
||||||
"status": coordinator.data.status,
|
"status": door.status,
|
||||||
"link_status": coordinator.data.link_status,
|
"link_status": door.link_status,
|
||||||
"battery_level": coordinator.data.battery_level,
|
"battery_level": door.battery_level,
|
||||||
}
|
}
|
||||||
for uid, coordinator in config_entry.runtime_data.items()
|
for uid, door in config_entry.runtime_data.data.items()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Base class for Aladdin Connect entities."""
|
"""Base class for Aladdin Connect entities."""
|
||||||
|
|
||||||
from genie_partner_sdk.client import AladdinConnectClient
|
from genie_partner_sdk.client import AladdinConnectClient
|
||||||
|
from genie_partner_sdk.model import GarageDoor
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
@@ -14,17 +15,28 @@ class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]):
|
|||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
|
def __init__(self, coordinator: AladdinConnectCoordinator, door_id: str) -> None:
|
||||||
"""Initialize Aladdin Connect entity."""
|
"""Initialize Aladdin Connect entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
device = coordinator.data
|
self._door_id = door_id
|
||||||
|
door = self.door
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, device.unique_id)},
|
identifiers={(DOMAIN, door.unique_id)},
|
||||||
manufacturer="Aladdin Connect",
|
manufacturer="Aladdin Connect",
|
||||||
name=device.name,
|
name=door.name,
|
||||||
)
|
)
|
||||||
self._device_id = device.device_id
|
self._device_id = door.device_id
|
||||||
self._number = device.door_number
|
self._number = door.door_number
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return super().available and self._door_id in self.coordinator.data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def door(self) -> GarageDoor:
|
||||||
|
"""Return the garage door data."""
|
||||||
|
return self.coordinator.data[self._door_id]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def client(self) -> AladdinConnectClient:
|
def client(self) -> AladdinConnectClient:
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ rules:
|
|||||||
docs-supported-functions: done
|
docs-supported-functions: done
|
||||||
docs-troubleshooting: done
|
docs-troubleshooting: done
|
||||||
docs-use-cases: done
|
docs-use-cases: done
|
||||||
dynamic-devices: todo
|
dynamic-devices: done
|
||||||
entity-category: done
|
entity-category: done
|
||||||
entity-device-class: done
|
entity-device-class: done
|
||||||
entity-disabled-by-default: done
|
entity-disabled-by-default: done
|
||||||
@@ -66,9 +66,7 @@ rules:
|
|||||||
icon-translations: todo
|
icon-translations: todo
|
||||||
reconfiguration-flow: todo
|
reconfiguration-flow: todo
|
||||||
repair-issues: todo
|
repair-issues: todo
|
||||||
stale-devices:
|
stale-devices: done
|
||||||
status: todo
|
|
||||||
comment: We can automatically remove removed devices
|
|
||||||
|
|
||||||
# Platinum
|
# Platinum
|
||||||
async-dependency: todo
|
async-dependency: todo
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from homeassistant.components.sensor import (
|
|||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
|
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
|
||||||
@@ -49,13 +49,24 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Aladdin Connect sensor devices."""
|
"""Set up Aladdin Connect sensor devices."""
|
||||||
coordinators = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
|
known_devices: set[str] = set()
|
||||||
|
|
||||||
async_add_entities(
|
@callback
|
||||||
AladdinConnectSensor(coordinator, description)
|
def _async_add_new_devices() -> None:
|
||||||
for coordinator in coordinators.values()
|
"""Detect and add entities for new doors."""
|
||||||
for description in SENSOR_TYPES
|
current_devices = set(coordinator.data)
|
||||||
)
|
new_devices = current_devices - known_devices
|
||||||
|
if new_devices:
|
||||||
|
known_devices.update(new_devices)
|
||||||
|
async_add_entities(
|
||||||
|
AladdinConnectSensor(coordinator, door_id, description)
|
||||||
|
for door_id in new_devices
|
||||||
|
for description in SENSOR_TYPES
|
||||||
|
)
|
||||||
|
|
||||||
|
_async_add_new_devices()
|
||||||
|
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
|
||||||
|
|
||||||
|
|
||||||
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
|
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
|
||||||
@@ -66,14 +77,15 @@ class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: AladdinConnectCoordinator,
|
coordinator: AladdinConnectCoordinator,
|
||||||
|
door_id: str,
|
||||||
entity_description: AladdinConnectSensorEntityDescription,
|
entity_description: AladdinConnectSensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Aladdin Connect sensor."""
|
"""Initialize the Aladdin Connect sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator, door_id)
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
self._attr_unique_id = f"{coordinator.data.unique_id}-{entity_description.key}"
|
self._attr_unique_id = f"{door_id}-{entity_description.key}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> float | None:
|
def native_value(self) -> float | None:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return self.entity_description.value_fn(self.coordinator.data)
|
return self.entity_description.value_fn(self.door)
|
||||||
|
|||||||
@@ -37,6 +37,9 @@
|
|||||||
"close_door_failed": {
|
"close_door_failed": {
|
||||||
"message": "Failed to close the garage door"
|
"message": "Failed to close the garage door"
|
||||||
},
|
},
|
||||||
|
"oauth2_implementation_unavailable": {
|
||||||
|
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||||
|
},
|
||||||
"open_door_failed": {
|
"open_door_failed": {
|
||||||
"message": "Failed to open the garage door"
|
"message": "Failed to open the garage door"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.automation import DomainSpec
|
||||||
from homeassistant.helpers.condition import (
|
from homeassistant.helpers.condition import (
|
||||||
Condition,
|
Condition,
|
||||||
EntityStateConditionBase,
|
EntityStateConditionBase,
|
||||||
@@ -43,7 +44,7 @@ def make_entity_state_required_features_condition(
|
|||||||
class CustomCondition(EntityStateRequiredFeaturesCondition):
|
class CustomCondition(EntityStateRequiredFeaturesCondition):
|
||||||
"""Condition for entity state changes."""
|
"""Condition for entity state changes."""
|
||||||
|
|
||||||
_domain = domain
|
_domain_specs = {domain: DomainSpec()}
|
||||||
_states = {to_state}
|
_states = {to_state}
|
||||||
_required_features = required_features
|
_required_features = required_features
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
"condition_behavior_description": "How the state should match on the targeted alarms.",
|
"condition_behavior_name": "Condition passes if",
|
||||||
"condition_behavior_name": "Behavior",
|
"trigger_behavior_name": "Trigger when"
|
||||||
"trigger_behavior_description": "The behavior of the targeted alarms to trigger on.",
|
|
||||||
"trigger_behavior_name": "Behavior"
|
|
||||||
},
|
},
|
||||||
"conditions": {
|
"conditions": {
|
||||||
"is_armed": {
|
"is_armed": {
|
||||||
"description": "Tests if one or more alarms are armed.",
|
"description": "Tests if one or more alarms are armed.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -20,7 +17,6 @@
|
|||||||
"description": "Tests if one or more alarms are armed in away mode.",
|
"description": "Tests if one or more alarms are armed in away mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -30,7 +26,6 @@
|
|||||||
"description": "Tests if one or more alarms are armed in home mode.",
|
"description": "Tests if one or more alarms are armed in home mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -40,7 +35,6 @@
|
|||||||
"description": "Tests if one or more alarms are armed in night mode.",
|
"description": "Tests if one or more alarms are armed in night mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -50,7 +44,6 @@
|
|||||||
"description": "Tests if one or more alarms are armed in vacation mode.",
|
"description": "Tests if one or more alarms are armed in vacation mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -60,7 +53,6 @@
|
|||||||
"description": "Tests if one or more alarms are disarmed.",
|
"description": "Tests if one or more alarms are disarmed.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -70,7 +62,6 @@
|
|||||||
"description": "Tests if one or more alarms are triggered.",
|
"description": "Tests if one or more alarms are triggered.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -173,7 +164,7 @@
|
|||||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Arm away"
|
"name": "Arm alarm away"
|
||||||
},
|
},
|
||||||
"alarm_arm_custom_bypass": {
|
"alarm_arm_custom_bypass": {
|
||||||
"description": "Arms an alarm while allowing to bypass a custom area.",
|
"description": "Arms an alarm while allowing to bypass a custom area.",
|
||||||
@@ -183,7 +174,7 @@
|
|||||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Arm with custom bypass"
|
"name": "Arm alarm with custom bypass"
|
||||||
},
|
},
|
||||||
"alarm_arm_home": {
|
"alarm_arm_home": {
|
||||||
"description": "Arms an alarm in the home mode.",
|
"description": "Arms an alarm in the home mode.",
|
||||||
@@ -193,7 +184,7 @@
|
|||||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Arm home"
|
"name": "Arm alarm home"
|
||||||
},
|
},
|
||||||
"alarm_arm_night": {
|
"alarm_arm_night": {
|
||||||
"description": "Arms an alarm in the night mode.",
|
"description": "Arms an alarm in the night mode.",
|
||||||
@@ -203,7 +194,7 @@
|
|||||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Arm night"
|
"name": "Arm alarm night"
|
||||||
},
|
},
|
||||||
"alarm_arm_vacation": {
|
"alarm_arm_vacation": {
|
||||||
"description": "Arms an alarm in the vacation mode.",
|
"description": "Arms an alarm in the vacation mode.",
|
||||||
@@ -213,7 +204,7 @@
|
|||||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Arm vacation"
|
"name": "Arm alarm vacation"
|
||||||
},
|
},
|
||||||
"alarm_disarm": {
|
"alarm_disarm": {
|
||||||
"description": "Disarms an alarm.",
|
"description": "Disarms an alarm.",
|
||||||
@@ -223,7 +214,7 @@
|
|||||||
"name": "Code"
|
"name": "Code"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Disarm"
|
"name": "Disarm alarm"
|
||||||
},
|
},
|
||||||
"alarm_trigger": {
|
"alarm_trigger": {
|
||||||
"description": "Triggers an alarm manually.",
|
"description": "Triggers an alarm manually.",
|
||||||
@@ -233,7 +224,7 @@
|
|||||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Trigger"
|
"name": "Trigger alarm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Alarm control panel",
|
"title": "Alarm control panel",
|
||||||
@@ -242,7 +233,6 @@
|
|||||||
"description": "Triggers after one or more alarms become armed, regardless of the mode.",
|
"description": "Triggers after one or more alarms become armed, regardless of the mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -252,7 +242,6 @@
|
|||||||
"description": "Triggers after one or more alarms become armed in away mode.",
|
"description": "Triggers after one or more alarms become armed in away mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -262,7 +251,6 @@
|
|||||||
"description": "Triggers after one or more alarms become armed in home mode.",
|
"description": "Triggers after one or more alarms become armed in home mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -272,7 +260,6 @@
|
|||||||
"description": "Triggers after one or more alarms become armed in night mode.",
|
"description": "Triggers after one or more alarms become armed in night mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -282,7 +269,6 @@
|
|||||||
"description": "Triggers after one or more alarms become armed in vacation mode.",
|
"description": "Triggers after one or more alarms become armed in vacation mode.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -292,7 +278,6 @@
|
|||||||
"description": "Triggers after one or more alarms become disarmed.",
|
"description": "Triggers after one or more alarms become disarmed.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -302,7 +287,6 @@
|
|||||||
"description": "Triggers after one or more alarms become triggered.",
|
"description": "Triggers after one or more alarms become triggered.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.automation import DomainSpec
|
||||||
from homeassistant.helpers.entity import get_supported_features
|
from homeassistant.helpers.entity import get_supported_features
|
||||||
from homeassistant.helpers.trigger import (
|
from homeassistant.helpers.trigger import (
|
||||||
EntityTargetStateTriggerBase,
|
EntityTargetStateTriggerBase,
|
||||||
@@ -44,7 +45,7 @@ def make_entity_state_trigger_required_features(
|
|||||||
class CustomTrigger(EntityStateTriggerRequiredFeatures):
|
class CustomTrigger(EntityStateTriggerRequiredFeatures):
|
||||||
"""Trigger for entity state changes."""
|
"""Trigger for entity state changes."""
|
||||||
|
|
||||||
_domain = domain
|
_domain_specs = {domain: DomainSpec()}
|
||||||
_to_states = {to_state}
|
_to_states = {to_state}
|
||||||
_required_features = required_features
|
_required_features = required_features
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
|
|
||||||
from adext import AdExt
|
from adext import AdExt
|
||||||
from alarmdecoder.devices import SerialDevice, SocketDevice
|
from alarmdecoder.devices import Device, SerialDevice, SocketDevice
|
||||||
from alarmdecoder.util import NoDeviceError
|
from alarmdecoder.util import NoDeviceError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -102,16 +102,21 @@ class AlarmDecoderFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
self._async_current_entries(), user_input, self.protocol
|
self._async_current_entries(), user_input, self.protocol
|
||||||
):
|
):
|
||||||
return self.async_abort(reason="already_configured")
|
return self.async_abort(reason="already_configured")
|
||||||
connection = {}
|
connection: dict[str, Any] = {}
|
||||||
baud = None
|
baud = None
|
||||||
|
device: Device
|
||||||
if self.protocol == PROTOCOL_SOCKET:
|
if self.protocol == PROTOCOL_SOCKET:
|
||||||
host = connection[CONF_HOST] = user_input[CONF_HOST]
|
host = connection[CONF_HOST] = cast(str, user_input[CONF_HOST])
|
||||||
port = connection[CONF_PORT] = user_input[CONF_PORT]
|
port = connection[CONF_PORT] = cast(int, user_input[CONF_PORT])
|
||||||
title = f"{host}:{port}"
|
title: str = f"{host}:{port}"
|
||||||
device = SocketDevice(interface=(host, port))
|
device = SocketDevice(interface=(host, port))
|
||||||
if self.protocol == PROTOCOL_SERIAL:
|
if self.protocol == PROTOCOL_SERIAL:
|
||||||
path = connection[CONF_DEVICE_PATH] = user_input[CONF_DEVICE_PATH]
|
path = connection[CONF_DEVICE_PATH] = cast(
|
||||||
baud = connection[CONF_DEVICE_BAUD] = user_input[CONF_DEVICE_BAUD]
|
str, user_input[CONF_DEVICE_PATH]
|
||||||
|
)
|
||||||
|
baud = connection[CONF_DEVICE_BAUD] = cast(
|
||||||
|
int, user_input[CONF_DEVICE_BAUD]
|
||||||
|
)
|
||||||
title = path
|
title = path
|
||||||
device = SerialDevice(interface=path)
|
device = SerialDevice(interface=path)
|
||||||
|
|
||||||
@@ -132,6 +137,7 @@ class AlarmDecoderFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
_LOGGER.exception("Unexpected exception during AlarmDecoder setup")
|
_LOGGER.exception("Unexpected exception during AlarmDecoder setup")
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
schema: vol.Schema
|
||||||
if self.protocol == PROTOCOL_SOCKET:
|
if self.protocol == PROTOCOL_SOCKET:
|
||||||
schema = vol.Schema(
|
schema = vol.Schema(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Defines a base Alexa Devices entity."""
|
"""Defines a base Alexa Devices entity."""
|
||||||
|
|
||||||
from aioamazondevices.const.devices import SPEAKER_GROUP_MODEL
|
|
||||||
from aioamazondevices.structures import AmazonDevice
|
from aioamazondevices.structures import AmazonDevice
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
@@ -25,19 +24,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
|||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._serial_num = serial_num
|
self._serial_num = serial_num
|
||||||
model_details = coordinator.api.get_model_details(self.device) or {}
|
|
||||||
model = model_details.get("model")
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, serial_num)},
|
identifiers={(DOMAIN, serial_num)},
|
||||||
name=self.device.account_name,
|
name=self.device.account_name,
|
||||||
model=model,
|
model=self.device.model,
|
||||||
model_id=self.device.device_type,
|
model_id=self.device.device_type,
|
||||||
manufacturer=model_details.get("manufacturer", "Amazon"),
|
manufacturer=self.device.manufacturer or "Amazon",
|
||||||
hw_version=model_details.get("hw_version"),
|
hw_version=self.device.hardware_version,
|
||||||
sw_version=(
|
sw_version=self.device.software_version,
|
||||||
self.device.software_version if model != SPEAKER_GROUP_MODEL else None
|
serial_number=serial_num,
|
||||||
),
|
|
||||||
serial_number=serial_num if model != SPEAKER_GROUP_MODEL else None,
|
|
||||||
)
|
)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_unique_id = f"{serial_num}-{description.key}"
|
self._attr_unique_id = f"{serial_num}-{description.key}"
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioamazondevices"],
|
"loggers": ["aioamazondevices"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aioamazondevices==12.0.2"]
|
"requirements": ["aioamazondevices==13.3.1"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,10 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
|
|||||||
assert method is not None
|
assert method is not None
|
||||||
|
|
||||||
await method(self.device, state)
|
await method(self.device, state)
|
||||||
await self.coordinator.async_request_refresh()
|
self.coordinator.data[self.device.serial_number].sensors[
|
||||||
|
self.entity_description.key
|
||||||
|
].value = state
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the switch on."""
|
"""Turn the switch on."""
|
||||||
|
|||||||
@@ -338,6 +338,7 @@ class Analytics:
|
|||||||
|
|
||||||
hass = self._hass
|
hass = self._hass
|
||||||
supervisor_info = None
|
supervisor_info = None
|
||||||
|
addons_info: dict[str, Any] | None = None
|
||||||
operating_system_info: dict[str, Any] = {}
|
operating_system_info: dict[str, Any] = {}
|
||||||
|
|
||||||
if self._data.uuid is None:
|
if self._data.uuid is None:
|
||||||
@@ -347,6 +348,7 @@ class Analytics:
|
|||||||
if self.supervisor:
|
if self.supervisor:
|
||||||
supervisor_info = hassio.get_supervisor_info(hass)
|
supervisor_info = hassio.get_supervisor_info(hass)
|
||||||
operating_system_info = hassio.get_os_info(hass) or {}
|
operating_system_info = hassio.get_os_info(hass) or {}
|
||||||
|
addons_info = hassio.get_addons_info(hass) or {}
|
||||||
|
|
||||||
system_info = await async_get_system_info(hass)
|
system_info = await async_get_system_info(hass)
|
||||||
integrations = []
|
integrations = []
|
||||||
@@ -419,13 +421,10 @@ class Analytics:
|
|||||||
|
|
||||||
integrations.append(integration.domain)
|
integrations.append(integration.domain)
|
||||||
|
|
||||||
if supervisor_info is not None:
|
if addons_info is not None:
|
||||||
supervisor_client = hassio.get_supervisor_client(hass)
|
supervisor_client = hassio.get_supervisor_client(hass)
|
||||||
installed_addons = await asyncio.gather(
|
installed_addons = await asyncio.gather(
|
||||||
*(
|
*(supervisor_client.addons.addon_info(slug) for slug in addons_info)
|
||||||
supervisor_client.addons.addon_info(addon[ATTR_SLUG])
|
|
||||||
for addon in supervisor_info[ATTR_ADDONS]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
addons.extend(
|
addons.extend(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from python_homeassistant_analytics import (
|
from python_homeassistant_analytics import (
|
||||||
|
Environment,
|
||||||
HomeassistantAnalyticsClient,
|
HomeassistantAnalyticsClient,
|
||||||
HomeassistantAnalyticsConnectionError,
|
HomeassistantAnalyticsConnectionError,
|
||||||
)
|
)
|
||||||
@@ -38,7 +39,7 @@ async def async_setup_entry(
|
|||||||
client = HomeassistantAnalyticsClient(session=async_get_clientsession(hass))
|
client = HomeassistantAnalyticsClient(session=async_get_clientsession(hass))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
integrations = await client.get_integrations()
|
integrations = await client.get_integrations(Environment.NEXT)
|
||||||
except HomeassistantAnalyticsConnectionError as ex:
|
except HomeassistantAnalyticsConnectionError as ex:
|
||||||
raise ConfigEntryNotReady("Could not fetch integration list") from ex
|
raise ConfigEntryNotReady("Could not fetch integration list") from ex
|
||||||
|
|
||||||
|
|||||||
@@ -27,4 +27,4 @@ def create_api(hass: HomeAssistant, host: str, enable_ime: bool) -> AndroidTVRem
|
|||||||
|
|
||||||
def get_enable_ime(entry: AndroidTVRemoteConfigEntry) -> bool:
|
def get_enable_ime(entry: AndroidTVRemoteConfigEntry) -> bool:
|
||||||
"""Get value of enable_ime option or its default value."""
|
"""Get value of enable_ime option or its default value."""
|
||||||
return entry.options.get(CONF_ENABLE_IME, CONF_ENABLE_IME_DEFAULT_VALUE) # type: ignore[no-any-return]
|
return bool(entry.options.get(CONF_ENABLE_IME, CONF_ENABLE_IME_DEFAULT_VALUE))
|
||||||
|
|||||||
@@ -8,6 +8,6 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["androidtvremote2"],
|
"loggers": ["androidtvremote2"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["androidtvremote2==0.2.3"],
|
"requirements": ["androidtvremote2==0.3.1"],
|
||||||
"zeroconf": ["_androidtvremote2._tcp.local."]
|
"zeroconf": ["_androidtvremote2._tcp.local."]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyanglianwater"],
|
"loggers": ["pyanglianwater"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pyanglianwater==3.1.0"]
|
"requirements": ["pyanglianwater==3.1.2"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
|
||||||
from pyanglianwater.meter import SmartMeter
|
from pyanglianwater.meter import SmartMeter
|
||||||
@@ -32,13 +33,14 @@ class AnglianWaterSensor(StrEnum):
|
|||||||
YESTERDAY_WATER_COST = "yesterday_water_cost"
|
YESTERDAY_WATER_COST = "yesterday_water_cost"
|
||||||
YESTERDAY_SEWERAGE_COST = "yesterday_sewerage_cost"
|
YESTERDAY_SEWERAGE_COST = "yesterday_sewerage_cost"
|
||||||
LATEST_READING = "latest_reading"
|
LATEST_READING = "latest_reading"
|
||||||
|
LAST_UPDATED = "last_updated"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class AnglianWaterSensorEntityDescription(SensorEntityDescription):
|
class AnglianWaterSensorEntityDescription(SensorEntityDescription):
|
||||||
"""Describes AnglianWater sensor entity."""
|
"""Describes AnglianWater sensor entity."""
|
||||||
|
|
||||||
value_fn: Callable[[SmartMeter], float]
|
value_fn: Callable[[SmartMeter], float | datetime | None]
|
||||||
|
|
||||||
|
|
||||||
ENTITY_DESCRIPTIONS: tuple[AnglianWaterSensorEntityDescription, ...] = (
|
ENTITY_DESCRIPTIONS: tuple[AnglianWaterSensorEntityDescription, ...] = (
|
||||||
@@ -76,6 +78,13 @@ ENTITY_DESCRIPTIONS: tuple[AnglianWaterSensorEntityDescription, ...] = (
|
|||||||
translation_key=AnglianWaterSensor.YESTERDAY_SEWERAGE_COST,
|
translation_key=AnglianWaterSensor.YESTERDAY_SEWERAGE_COST,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
|
AnglianWaterSensorEntityDescription(
|
||||||
|
key=AnglianWaterSensor.LAST_UPDATED,
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
value_fn=lambda entity: entity.last_updated,
|
||||||
|
translation_key=AnglianWaterSensor.LAST_UPDATED,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -112,6 +121,6 @@ class AnglianWaterSensorEntity(AnglianWaterEntity, SensorEntity):
|
|||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> float | None:
|
def native_value(self) -> float | datetime | None:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return self.entity_description.value_fn(self.smart_meter)
|
return self.entity_description.value_fn(self.smart_meter)
|
||||||
|
|||||||
@@ -34,6 +34,9 @@
|
|||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
|
"last_updated": {
|
||||||
|
"name": "Last meter reading processed"
|
||||||
|
},
|
||||||
"latest_reading": {
|
"latest_reading": {
|
||||||
"name": "Latest reading"
|
"name": "Latest reading"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,9 +45,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
|
|||||||
try:
|
try:
|
||||||
await client.models.list(timeout=10.0)
|
await client.models.list(timeout=10.0)
|
||||||
except anthropic.AuthenticationError as err:
|
except anthropic.AuthenticationError as err:
|
||||||
raise ConfigEntryAuthFailed(err) from err
|
raise ConfigEntryAuthFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="api_authentication_error",
|
||||||
|
translation_placeholders={"message": err.message},
|
||||||
|
) from err
|
||||||
except anthropic.AnthropicError as err:
|
except anthropic.AnthropicError as err:
|
||||||
raise ConfigEntryNotReady(err) from err
|
raise ConfigEntryNotReady(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="api_error",
|
||||||
|
translation_placeholders={
|
||||||
|
"message": err.message
|
||||||
|
if isinstance(err, anthropic.APIError)
|
||||||
|
else str(err)
|
||||||
|
},
|
||||||
|
) from err
|
||||||
|
|
||||||
entry.runtime_data = client
|
entry.runtime_data = client
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from homeassistant.exceptions import HomeAssistantError
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.util.json import json_loads
|
from homeassistant.util.json import json_loads
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
from .entity import AnthropicBaseLLMEntity
|
from .entity import AnthropicBaseLLMEntity
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -60,7 +61,7 @@ class AnthropicTaskEntity(
|
|||||||
|
|
||||||
if not isinstance(chat_log.content[-1], conversation.AssistantContent):
|
if not isinstance(chat_log.content[-1], conversation.AssistantContent):
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
"Last content in chat log is not an AssistantContent"
|
translation_domain=DOMAIN, translation_key="response_not_found"
|
||||||
)
|
)
|
||||||
|
|
||||||
text = chat_log.content[-1].content or ""
|
text = chat_log.content[-1].content or ""
|
||||||
@@ -78,7 +79,9 @@ class AnthropicTaskEntity(
|
|||||||
err,
|
err,
|
||||||
text,
|
text,
|
||||||
)
|
)
|
||||||
raise HomeAssistantError("Error with Claude structured response") from err
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN, translation_key="json_parse_error"
|
||||||
|
) from err
|
||||||
|
|
||||||
return ai_task.GenDataTaskResult(
|
return ai_task.GenDataTaskResult(
|
||||||
conversation_id=chat_log.conversation_id,
|
conversation_id=chat_log.conversation_id,
|
||||||
|
|||||||
@@ -71,6 +71,16 @@ CODE_EXECUTION_UNSUPPORTED_MODELS = [
|
|||||||
"claude-3-haiku",
|
"claude-3-haiku",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS = [
|
||||||
|
"claude-haiku-4-5",
|
||||||
|
"claude-opus-4-1",
|
||||||
|
"claude-opus-4-0",
|
||||||
|
"claude-opus-4-20250514",
|
||||||
|
"claude-sonnet-4-0",
|
||||||
|
"claude-sonnet-4-20250514",
|
||||||
|
"claude-3-haiku",
|
||||||
|
]
|
||||||
|
|
||||||
DEPRECATED_MODELS = [
|
DEPRECATED_MODELS = [
|
||||||
"claude-3",
|
"claude-3",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""Diagnostics support for Anthropic."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
from anthropic import __title__, __version__
|
||||||
|
|
||||||
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
|
from homeassistant.const import CONF_API_KEY
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
CONF_PROMPT,
|
||||||
|
CONF_WEB_SEARCH_CITY,
|
||||||
|
CONF_WEB_SEARCH_COUNTRY,
|
||||||
|
CONF_WEB_SEARCH_REGION,
|
||||||
|
CONF_WEB_SEARCH_TIMEZONE,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import AnthropicConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
TO_REDACT = {
|
||||||
|
CONF_API_KEY,
|
||||||
|
CONF_PROMPT,
|
||||||
|
CONF_WEB_SEARCH_CITY,
|
||||||
|
CONF_WEB_SEARCH_REGION,
|
||||||
|
CONF_WEB_SEARCH_COUNTRY,
|
||||||
|
CONF_WEB_SEARCH_TIMEZONE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_config_entry_diagnostics(
|
||||||
|
hass: HomeAssistant, entry: AnthropicConfigEntry
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return diagnostics for a config entry."""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"client": f"{__title__}=={__version__}",
|
||||||
|
"title": entry.title,
|
||||||
|
"entry_id": entry.entry_id,
|
||||||
|
"entry_version": f"{entry.version}.{entry.minor_version}",
|
||||||
|
"state": entry.state.value,
|
||||||
|
"data": async_redact_data(entry.data, TO_REDACT),
|
||||||
|
"options": async_redact_data(entry.options, TO_REDACT),
|
||||||
|
"subentries": {
|
||||||
|
subentry.subentry_id: {
|
||||||
|
"title": subentry.title,
|
||||||
|
"subentry_type": subentry.subentry_type,
|
||||||
|
"data": async_redact_data(subentry.data, TO_REDACT),
|
||||||
|
}
|
||||||
|
for subentry in entry.subentries.values()
|
||||||
|
},
|
||||||
|
"entities": {
|
||||||
|
entity_entry.entity_id: entity_entry.extended_dict
|
||||||
|
for entity_entry in er.async_entries_for_config_entry(
|
||||||
|
er.async_get(hass), entry.entry_id
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@ from anthropic.types import (
|
|||||||
CitationsWebSearchResultLocation,
|
CitationsWebSearchResultLocation,
|
||||||
CitationWebSearchResultLocationParam,
|
CitationWebSearchResultLocationParam,
|
||||||
CodeExecutionTool20250825Param,
|
CodeExecutionTool20250825Param,
|
||||||
|
CodeExecutionToolResultBlock,
|
||||||
|
CodeExecutionToolResultBlockParamContentParam,
|
||||||
Container,
|
Container,
|
||||||
ContentBlockParam,
|
ContentBlockParam,
|
||||||
DocumentBlockParam,
|
DocumentBlockParam,
|
||||||
@@ -61,15 +63,16 @@ from anthropic.types import (
|
|||||||
ToolUseBlockParam,
|
ToolUseBlockParam,
|
||||||
Usage,
|
Usage,
|
||||||
WebSearchTool20250305Param,
|
WebSearchTool20250305Param,
|
||||||
|
WebSearchTool20260209Param,
|
||||||
WebSearchToolResultBlock,
|
WebSearchToolResultBlock,
|
||||||
WebSearchToolResultBlockParamContentParam,
|
WebSearchToolResultBlockParamContentParam,
|
||||||
)
|
)
|
||||||
from anthropic.types.bash_code_execution_tool_result_block_param import (
|
from anthropic.types.bash_code_execution_tool_result_block_param import (
|
||||||
Content as BashCodeExecutionToolResultContentParam,
|
Content as BashCodeExecutionToolResultBlockParamContentParam,
|
||||||
)
|
)
|
||||||
from anthropic.types.message_create_params import MessageCreateParamsStreaming
|
from anthropic.types.message_create_params import MessageCreateParamsStreaming
|
||||||
from anthropic.types.text_editor_code_execution_tool_result_block_param import (
|
from anthropic.types.text_editor_code_execution_tool_result_block_param import (
|
||||||
Content as TextEditorCodeExecutionToolResultContentParam,
|
Content as TextEditorCodeExecutionToolResultBlockParamContentParam,
|
||||||
)
|
)
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous_openapi import convert
|
from voluptuous_openapi import convert
|
||||||
@@ -105,6 +108,7 @@ from .const import (
|
|||||||
MIN_THINKING_BUDGET,
|
MIN_THINKING_BUDGET,
|
||||||
NON_ADAPTIVE_THINKING_MODELS,
|
NON_ADAPTIVE_THINKING_MODELS,
|
||||||
NON_THINKING_MODELS,
|
NON_THINKING_MODELS,
|
||||||
|
PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS,
|
||||||
UNSUPPORTED_STRUCTURED_OUTPUT_MODELS,
|
UNSUPPORTED_STRUCTURED_OUTPUT_MODELS,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -224,12 +228,22 @@ def _convert_content(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
elif content.tool_name == "code_execution":
|
||||||
|
tool_result_block = {
|
||||||
|
"type": "code_execution_tool_result",
|
||||||
|
"tool_use_id": content.tool_call_id,
|
||||||
|
"content": cast(
|
||||||
|
CodeExecutionToolResultBlockParamContentParam,
|
||||||
|
content.tool_result,
|
||||||
|
),
|
||||||
|
}
|
||||||
elif content.tool_name == "bash_code_execution":
|
elif content.tool_name == "bash_code_execution":
|
||||||
tool_result_block = {
|
tool_result_block = {
|
||||||
"type": "bash_code_execution_tool_result",
|
"type": "bash_code_execution_tool_result",
|
||||||
"tool_use_id": content.tool_call_id,
|
"tool_use_id": content.tool_call_id,
|
||||||
"content": cast(
|
"content": cast(
|
||||||
BashCodeExecutionToolResultContentParam, content.tool_result
|
BashCodeExecutionToolResultBlockParamContentParam,
|
||||||
|
content.tool_result,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
elif content.tool_name == "text_editor_code_execution":
|
elif content.tool_name == "text_editor_code_execution":
|
||||||
@@ -237,7 +251,7 @@ def _convert_content(
|
|||||||
"type": "text_editor_code_execution_tool_result",
|
"type": "text_editor_code_execution_tool_result",
|
||||||
"tool_use_id": content.tool_call_id,
|
"tool_use_id": content.tool_call_id,
|
||||||
"content": cast(
|
"content": cast(
|
||||||
TextEditorCodeExecutionToolResultContentParam,
|
TextEditorCodeExecutionToolResultBlockParamContentParam,
|
||||||
content.tool_result,
|
content.tool_result,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -368,6 +382,7 @@ def _convert_content(
|
|||||||
name=cast(
|
name=cast(
|
||||||
Literal[
|
Literal[
|
||||||
"web_search",
|
"web_search",
|
||||||
|
"code_execution",
|
||||||
"bash_code_execution",
|
"bash_code_execution",
|
||||||
"text_editor_code_execution",
|
"text_editor_code_execution",
|
||||||
],
|
],
|
||||||
@@ -379,6 +394,7 @@ def _convert_content(
|
|||||||
and tool_call.tool_name
|
and tool_call.tool_name
|
||||||
in [
|
in [
|
||||||
"web_search",
|
"web_search",
|
||||||
|
"code_execution",
|
||||||
"bash_code_execution",
|
"bash_code_execution",
|
||||||
"text_editor_code_execution",
|
"text_editor_code_execution",
|
||||||
]
|
]
|
||||||
@@ -401,7 +417,11 @@ def _convert_content(
|
|||||||
messages[-1]["content"] = messages[-1]["content"][0]["text"]
|
messages[-1]["content"] = messages[-1]["content"][0]["text"]
|
||||||
else:
|
else:
|
||||||
# Note: We don't pass SystemContent here as it's passed to the API as the prompt
|
# Note: We don't pass SystemContent here as it's passed to the API as the prompt
|
||||||
raise HomeAssistantError("Unexpected content type in chat log")
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="unexpected_chat_log_content",
|
||||||
|
translation_placeholders={"type": type(content).__name__},
|
||||||
|
)
|
||||||
|
|
||||||
return messages, container_id
|
return messages, container_id
|
||||||
|
|
||||||
@@ -443,7 +463,9 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
|
|||||||
Each message could contain multiple blocks of the same type.
|
Each message could contain multiple blocks of the same type.
|
||||||
"""
|
"""
|
||||||
if stream is None or not hasattr(stream, "__aiter__"):
|
if stream is None or not hasattr(stream, "__aiter__"):
|
||||||
raise HomeAssistantError("Expected a stream of messages")
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN, translation_key="unexpected_stream_object"
|
||||||
|
)
|
||||||
|
|
||||||
current_tool_block: ToolUseBlockParam | ServerToolUseBlockParam | None = None
|
current_tool_block: ToolUseBlockParam | ServerToolUseBlockParam | None = None
|
||||||
current_tool_args: str
|
current_tool_args: str
|
||||||
@@ -464,7 +486,7 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
|
|||||||
type="tool_use",
|
type="tool_use",
|
||||||
id=response.content_block.id,
|
id=response.content_block.id,
|
||||||
name=response.content_block.name,
|
name=response.content_block.name,
|
||||||
input={},
|
input=response.content_block.input or {},
|
||||||
)
|
)
|
||||||
current_tool_args = ""
|
current_tool_args = ""
|
||||||
if response.content_block.name == output_tool:
|
if response.content_block.name == output_tool:
|
||||||
@@ -526,13 +548,14 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
|
|||||||
type="server_tool_use",
|
type="server_tool_use",
|
||||||
id=response.content_block.id,
|
id=response.content_block.id,
|
||||||
name=response.content_block.name,
|
name=response.content_block.name,
|
||||||
input={},
|
input=response.content_block.input or {},
|
||||||
)
|
)
|
||||||
current_tool_args = ""
|
current_tool_args = ""
|
||||||
elif isinstance(
|
elif isinstance(
|
||||||
response.content_block,
|
response.content_block,
|
||||||
(
|
(
|
||||||
WebSearchToolResultBlock,
|
WebSearchToolResultBlock,
|
||||||
|
CodeExecutionToolResultBlock,
|
||||||
BashCodeExecutionToolResultBlock,
|
BashCodeExecutionToolResultBlock,
|
||||||
TextEditorCodeExecutionToolResultBlock,
|
TextEditorCodeExecutionToolResultBlock,
|
||||||
),
|
),
|
||||||
@@ -588,13 +611,13 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
|
|||||||
current_tool_block = None
|
current_tool_block = None
|
||||||
continue
|
continue
|
||||||
tool_args = json.loads(current_tool_args) if current_tool_args else {}
|
tool_args = json.loads(current_tool_args) if current_tool_args else {}
|
||||||
current_tool_block["input"] = tool_args
|
current_tool_block["input"] |= tool_args
|
||||||
yield {
|
yield {
|
||||||
"tool_calls": [
|
"tool_calls": [
|
||||||
llm.ToolInput(
|
llm.ToolInput(
|
||||||
id=current_tool_block["id"],
|
id=current_tool_block["id"],
|
||||||
tool_name=current_tool_block["name"],
|
tool_name=current_tool_block["name"],
|
||||||
tool_args=tool_args,
|
tool_args=current_tool_block["input"],
|
||||||
external=current_tool_block["type"] == "server_tool_use",
|
external=current_tool_block["type"] == "server_tool_use",
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -605,7 +628,9 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
|
|||||||
chat_log.async_trace(_create_token_stats(input_usage, usage))
|
chat_log.async_trace(_create_token_stats(input_usage, usage))
|
||||||
content_details.container = response.delta.container
|
content_details.container = response.delta.container
|
||||||
if response.delta.stop_reason == "refusal":
|
if response.delta.stop_reason == "refusal":
|
||||||
raise HomeAssistantError("Potential policy violation detected")
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN, translation_key="api_refusal"
|
||||||
|
)
|
||||||
elif isinstance(response, RawMessageStopEvent):
|
elif isinstance(response, RawMessageStopEvent):
|
||||||
if content_details:
|
if content_details:
|
||||||
content_details.delete_empty()
|
content_details.delete_empty()
|
||||||
@@ -664,7 +689,9 @@ class AnthropicBaseLLMEntity(Entity):
|
|||||||
|
|
||||||
system = chat_log.content[0]
|
system = chat_log.content[0]
|
||||||
if not isinstance(system, conversation.SystemContent):
|
if not isinstance(system, conversation.SystemContent):
|
||||||
raise HomeAssistantError("First message must be a system message")
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN, translation_key="system_message_not_found"
|
||||||
|
)
|
||||||
|
|
||||||
# System prompt with caching enabled
|
# System prompt with caching enabled
|
||||||
system_prompt: list[TextBlockParam] = [
|
system_prompt: list[TextBlockParam] = [
|
||||||
@@ -725,19 +752,34 @@ class AnthropicBaseLLMEntity(Entity):
|
|||||||
]
|
]
|
||||||
|
|
||||||
if options.get(CONF_CODE_EXECUTION):
|
if options.get(CONF_CODE_EXECUTION):
|
||||||
tools.append(
|
# The `web_search_20260209` tool automatically enables `code_execution_20260120` tool
|
||||||
CodeExecutionTool20250825Param(
|
if model.startswith(
|
||||||
name="code_execution",
|
tuple(PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS)
|
||||||
type="code_execution_20250825",
|
) or not options.get(CONF_WEB_SEARCH):
|
||||||
),
|
tools.append(
|
||||||
)
|
CodeExecutionTool20250825Param(
|
||||||
|
name="code_execution",
|
||||||
|
type="code_execution_20250825",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if options.get(CONF_WEB_SEARCH):
|
if options.get(CONF_WEB_SEARCH):
|
||||||
web_search = WebSearchTool20250305Param(
|
if model.startswith(
|
||||||
name="web_search",
|
tuple(PROGRAMMATIC_TOOL_CALLING_UNSUPPORTED_MODELS)
|
||||||
type="web_search_20250305",
|
) or not options.get(CONF_CODE_EXECUTION):
|
||||||
max_uses=options.get(CONF_WEB_SEARCH_MAX_USES),
|
web_search: WebSearchTool20250305Param | WebSearchTool20260209Param = (
|
||||||
)
|
WebSearchTool20250305Param(
|
||||||
|
name="web_search",
|
||||||
|
type="web_search_20250305",
|
||||||
|
max_uses=options.get(CONF_WEB_SEARCH_MAX_USES),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
web_search = WebSearchTool20260209Param(
|
||||||
|
name="web_search",
|
||||||
|
type="web_search_20260209",
|
||||||
|
max_uses=options.get(CONF_WEB_SEARCH_MAX_USES),
|
||||||
|
)
|
||||||
if options.get(CONF_WEB_SEARCH_USER_LOCATION):
|
if options.get(CONF_WEB_SEARCH_USER_LOCATION):
|
||||||
web_search["user_location"] = {
|
web_search["user_location"] = {
|
||||||
"type": "approximate",
|
"type": "approximate",
|
||||||
@@ -754,7 +796,7 @@ class AnthropicBaseLLMEntity(Entity):
|
|||||||
last_message = messages[-1]
|
last_message = messages[-1]
|
||||||
if last_message["role"] != "user":
|
if last_message["role"] != "user":
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
"Last message must be a user message to add attachments"
|
translation_domain=DOMAIN, translation_key="user_message_not_found"
|
||||||
)
|
)
|
||||||
if isinstance(last_message["content"], str):
|
if isinstance(last_message["content"], str):
|
||||||
last_message["content"] = [
|
last_message["content"] = [
|
||||||
@@ -859,11 +901,19 @@ class AnthropicBaseLLMEntity(Entity):
|
|||||||
except anthropic.AuthenticationError as err:
|
except anthropic.AuthenticationError as err:
|
||||||
self.entry.async_start_reauth(self.hass)
|
self.entry.async_start_reauth(self.hass)
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
"Authentication error with Anthropic API, reauthentication required"
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="api_authentication_error",
|
||||||
|
translation_placeholders={"message": err.message},
|
||||||
) from err
|
) from err
|
||||||
except anthropic.AnthropicError as err:
|
except anthropic.AnthropicError as err:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
f"Sorry, I had a problem talking to Anthropic: {err}"
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="api_error",
|
||||||
|
translation_placeholders={
|
||||||
|
"message": err.message
|
||||||
|
if isinstance(err, anthropic.APIError)
|
||||||
|
else str(err)
|
||||||
|
},
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
if not chat_log.unresponded_tool_results:
|
if not chat_log.unresponded_tool_results:
|
||||||
@@ -883,15 +933,23 @@ async def async_prepare_files_for_prompt(
|
|||||||
|
|
||||||
for file_path, mime_type in files:
|
for file_path, mime_type in files:
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
raise HomeAssistantError(f"`{file_path}` does not exist")
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="wrong_file_path",
|
||||||
|
translation_placeholders={"file_path": file_path.as_posix()},
|
||||||
|
)
|
||||||
|
|
||||||
if mime_type is None:
|
if mime_type is None:
|
||||||
mime_type = guess_file_type(file_path)[0]
|
mime_type = guess_file_type(file_path)[0]
|
||||||
|
|
||||||
if not mime_type or not mime_type.startswith(("image/", "application/pdf")):
|
if not mime_type or not mime_type.startswith(("image/", "application/pdf")):
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
"Only images and PDF are supported by the Anthropic API,"
|
translation_domain=DOMAIN,
|
||||||
f"`{file_path}` is not an image file or PDF"
|
translation_key="wrong_file_type",
|
||||||
|
translation_placeholders={
|
||||||
|
"file_path": file_path.as_posix(),
|
||||||
|
"mime_type": mime_type or "unknown",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if mime_type == "image/jpg":
|
if mime_type == "image/jpg":
|
||||||
mime_type = "image/jpeg"
|
mime_type = "image/jpeg"
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ rules:
|
|||||||
test-coverage: done
|
test-coverage: done
|
||||||
# Gold
|
# Gold
|
||||||
devices: done
|
devices: done
|
||||||
diagnostics: todo
|
diagnostics: done
|
||||||
discovery-update-info:
|
discovery-update-info:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: |
|
comment: |
|
||||||
@@ -59,17 +59,11 @@ rules:
|
|||||||
status: exempt
|
status: exempt
|
||||||
comment: |
|
comment: |
|
||||||
No data updates.
|
No data updates.
|
||||||
docs-examples:
|
docs-examples: done
|
||||||
status: todo
|
|
||||||
comment: |
|
|
||||||
To give examples of how people use the integration
|
|
||||||
docs-known-limitations: done
|
docs-known-limitations: done
|
||||||
docs-supported-devices:
|
docs-supported-devices: done
|
||||||
status: todo
|
|
||||||
comment: |
|
|
||||||
To write something about what models we support.
|
|
||||||
docs-supported-functions: done
|
docs-supported-functions: done
|
||||||
docs-troubleshooting: todo
|
docs-troubleshooting: done
|
||||||
docs-use-cases: done
|
docs-use-cases: done
|
||||||
dynamic-devices:
|
dynamic-devices:
|
||||||
status: exempt
|
status: exempt
|
||||||
@@ -88,7 +82,7 @@ rules:
|
|||||||
comment: |
|
comment: |
|
||||||
No entities disabled by default.
|
No entities disabled by default.
|
||||||
entity-translations: todo
|
entity-translations: todo
|
||||||
exception-translations: todo
|
exception-translations: done
|
||||||
icon-translations: done
|
icon-translations: done
|
||||||
reconfiguration-flow: done
|
reconfiguration-flow: done
|
||||||
repair-issues: done
|
repair-issues: done
|
||||||
|
|||||||
@@ -161,7 +161,9 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
|
|||||||
is None
|
is None
|
||||||
or (subentry := entry.subentries.get(self._current_subentry_id)) is None
|
or (subentry := entry.subentries.get(self._current_subentry_id)) is None
|
||||||
):
|
):
|
||||||
raise HomeAssistantError("Subentry not found")
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN, translation_key="subentry_not_found"
|
||||||
|
)
|
||||||
|
|
||||||
updated_data = {
|
updated_data = {
|
||||||
**subentry.data,
|
**subentry.data,
|
||||||
@@ -190,4 +192,6 @@ async def async_create_fix_flow(
|
|||||||
"""Create flow."""
|
"""Create flow."""
|
||||||
if issue_id == "model_deprecated":
|
if issue_id == "model_deprecated":
|
||||||
return ModelDeprecatedRepairFlow()
|
return ModelDeprecatedRepairFlow()
|
||||||
raise HomeAssistantError("Unknown issue ID")
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN, translation_key="unknown_issue_id"
|
||||||
|
)
|
||||||
|
|||||||
@@ -149,6 +149,47 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"api_authentication_error": {
|
||||||
|
"message": "Authentication error with Anthropic API: {message}. Reauthentication required."
|
||||||
|
},
|
||||||
|
"api_error": {
|
||||||
|
"message": "Anthropic API error: {message}."
|
||||||
|
},
|
||||||
|
"api_refusal": {
|
||||||
|
"message": "Potential policy violation detected."
|
||||||
|
},
|
||||||
|
"json_parse_error": {
|
||||||
|
"message": "Error with Claude structured response."
|
||||||
|
},
|
||||||
|
"response_not_found": {
|
||||||
|
"message": "Last content in chat log is not an AssistantContent."
|
||||||
|
},
|
||||||
|
"subentry_not_found": {
|
||||||
|
"message": "Subentry not found."
|
||||||
|
},
|
||||||
|
"system_message_not_found": {
|
||||||
|
"message": "First message must be a system message."
|
||||||
|
},
|
||||||
|
"unexpected_chat_log_content": {
|
||||||
|
"message": "Unexpected content type in chat log: {type}."
|
||||||
|
},
|
||||||
|
"unexpected_stream_object": {
|
||||||
|
"message": "Expected a stream of messages."
|
||||||
|
},
|
||||||
|
"unknown_issue_id": {
|
||||||
|
"message": "Unknown issue ID."
|
||||||
|
},
|
||||||
|
"user_message_not_found": {
|
||||||
|
"message": "Last message must be a user message to add attachments."
|
||||||
|
},
|
||||||
|
"wrong_file_path": {
|
||||||
|
"message": "`{file_path}` does not exist."
|
||||||
|
},
|
||||||
|
"wrong_file_type": {
|
||||||
|
"message": "Only images and PDF are supported by the Anthropic API, `{file_path}` ({mime_type}) is not an image file or PDF."
|
||||||
|
}
|
||||||
|
},
|
||||||
"issues": {
|
"issues": {
|
||||||
"model_deprecated": {
|
"model_deprecated": {
|
||||||
"fix_flow": {
|
"fix_flow": {
|
||||||
|
|||||||
@@ -2,74 +2,83 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from asyncio import timeout
|
from asyncio import timeout
|
||||||
|
from contextlib import AsyncExitStack
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from arcam.fmj import ConnectionFailed
|
from arcam.fmj import ConnectionFailed
|
||||||
from arcam.fmj.client import Client
|
from arcam.fmj.client import Client
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
|
||||||
|
|
||||||
from .const import (
|
from .const import DEFAULT_SCAN_INTERVAL
|
||||||
DEFAULT_SCAN_INTERVAL,
|
from .coordinator import ArcamFmjConfigEntry, ArcamFmjCoordinator, ArcamFmjRuntimeData
|
||||||
SIGNAL_CLIENT_DATA,
|
|
||||||
SIGNAL_CLIENT_STARTED,
|
|
||||||
SIGNAL_CLIENT_STOPPED,
|
|
||||||
)
|
|
||||||
|
|
||||||
type ArcamFmjConfigEntry = ConfigEntry[Client]
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
PLATFORMS = [Platform.BINARY_SENSOR, Platform.MEDIA_PLAYER, Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ArcamFmjConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ArcamFmjConfigEntry) -> bool:
|
||||||
"""Set up config entry."""
|
"""Set up config entry."""
|
||||||
entry.runtime_data = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
|
client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
|
||||||
|
|
||||||
|
coordinators: dict[int, ArcamFmjCoordinator] = {}
|
||||||
|
for zone in (1, 2):
|
||||||
|
coordinator = ArcamFmjCoordinator(hass, entry, client, zone)
|
||||||
|
coordinators[zone] = coordinator
|
||||||
|
|
||||||
|
entry.runtime_data = ArcamFmjRuntimeData(client, coordinators)
|
||||||
|
|
||||||
entry.async_create_background_task(
|
entry.async_create_background_task(
|
||||||
hass, _run_client(hass, entry.runtime_data, DEFAULT_SCAN_INTERVAL), "arcam_fmj"
|
hass,
|
||||||
|
_run_client(hass, entry.runtime_data, DEFAULT_SCAN_INTERVAL),
|
||||||
|
"arcam_fmj",
|
||||||
)
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ArcamFmjConfigEntry) -> bool:
|
||||||
"""Cleanup before removing config entry."""
|
"""Cleanup before removing config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
async def _run_client(hass: HomeAssistant, client: Client, interval: float) -> None:
|
async def _run_client(
|
||||||
def _listen(_: Any) -> None:
|
hass: HomeAssistant,
|
||||||
async_dispatcher_send(hass, SIGNAL_CLIENT_DATA, client.host)
|
runtime_data: ArcamFmjRuntimeData,
|
||||||
|
interval: float,
|
||||||
|
) -> None:
|
||||||
|
client = runtime_data.client
|
||||||
|
coordinators = runtime_data.coordinators
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
async with timeout(interval):
|
async with AsyncExitStack() as stack:
|
||||||
await client.start()
|
async with timeout(interval):
|
||||||
|
await client.start()
|
||||||
|
stack.push_async_callback(client.stop)
|
||||||
|
|
||||||
_LOGGER.debug("Client connected %s", client.host)
|
_LOGGER.debug("Client connected %s", client.host)
|
||||||
async_dispatcher_send(hass, SIGNAL_CLIENT_STARTED, client.host)
|
|
||||||
|
try:
|
||||||
|
for coordinator in coordinators.values():
|
||||||
|
await stack.enter_async_context(
|
||||||
|
coordinator.async_monitor_client()
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
|
||||||
with client.listen(_listen):
|
|
||||||
await client.process()
|
await client.process()
|
||||||
finally:
|
finally:
|
||||||
await client.stop()
|
_LOGGER.debug("Client disconnected %s", client.host)
|
||||||
|
|
||||||
_LOGGER.debug("Client disconnected %s", client.host)
|
|
||||||
async_dispatcher_send(hass, SIGNAL_CLIENT_STOPPED, client.host)
|
|
||||||
|
|
||||||
except ConnectionFailed:
|
except ConnectionFailed:
|
||||||
await asyncio.sleep(interval)
|
pass
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Unexpected exception, aborting arcam client")
|
_LOGGER.exception("Unexpected exception, aborting arcam client")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
await asyncio.sleep(interval)
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""Arcam binary sensors for incoming stream info."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from arcam.fmj.state import State
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import (
|
||||||
|
BinarySensorEntity,
|
||||||
|
BinarySensorEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.const import EntityCategory
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import ArcamFmjConfigEntry
|
||||||
|
from .entity import ArcamFmjEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ArcamFmjBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||||
|
"""Describes an Arcam FMJ binary sensor entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[State], bool | None]
|
||||||
|
|
||||||
|
|
||||||
|
BINARY_SENSORS: tuple[ArcamFmjBinarySensorEntityDescription, ...] = (
|
||||||
|
ArcamFmjBinarySensorEntityDescription(
|
||||||
|
key="incoming_video_interlaced",
|
||||||
|
translation_key="incoming_video_interlaced",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda state: (
|
||||||
|
vp.interlaced
|
||||||
|
if (vp := state.get_incoming_video_parameters()) is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ArcamFmjConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Arcam FMJ binary sensors from a config entry."""
|
||||||
|
coordinators = config_entry.runtime_data.coordinators
|
||||||
|
|
||||||
|
entities: list[ArcamFmjBinarySensorEntity] = []
|
||||||
|
for coordinator in coordinators.values():
|
||||||
|
entities.extend(
|
||||||
|
ArcamFmjBinarySensorEntity(coordinator, description)
|
||||||
|
for description in BINARY_SENSORS
|
||||||
|
)
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class ArcamFmjBinarySensorEntity(ArcamFmjEntity, BinarySensorEntity):
|
||||||
|
"""Representation of an Arcam FMJ binary sensor."""
|
||||||
|
|
||||||
|
entity_description: ArcamFmjBinarySensorEntityDescription
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool | None:
|
||||||
|
"""Return the binary sensor value."""
|
||||||
|
return self.entity_description.value_fn(self.coordinator.state)
|
||||||
@@ -2,10 +2,6 @@
|
|||||||
|
|
||||||
DOMAIN = "arcam_fmj"
|
DOMAIN = "arcam_fmj"
|
||||||
|
|
||||||
SIGNAL_CLIENT_STARTED = "arcam.client_started"
|
|
||||||
SIGNAL_CLIENT_STOPPED = "arcam.client_stopped"
|
|
||||||
SIGNAL_CLIENT_DATA = "arcam.client_data"
|
|
||||||
|
|
||||||
EVENT_TURN_ON = "arcam_fmj.turn_on"
|
EVENT_TURN_ON = "arcam_fmj.turn_on"
|
||||||
|
|
||||||
DEFAULT_PORT = 50000
|
DEFAULT_PORT = 50000
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
"""Coordinator for Arcam FMJ integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from arcam.fmj import ConnectionFailed
|
||||||
|
from arcam.fmj.client import AmxDuetResponse, Client, ResponsePacket
|
||||||
|
from arcam.fmj.state import State
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ArcamFmjRuntimeData:
|
||||||
|
"""Runtime data for Arcam FMJ integration."""
|
||||||
|
|
||||||
|
client: Client
|
||||||
|
coordinators: dict[int, ArcamFmjCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
type ArcamFmjConfigEntry = ConfigEntry[ArcamFmjRuntimeData]
|
||||||
|
|
||||||
|
|
||||||
|
class ArcamFmjCoordinator(DataUpdateCoordinator[None]):
|
||||||
|
"""Coordinator for a single Arcam FMJ zone."""
|
||||||
|
|
||||||
|
config_entry: ArcamFmjConfigEntry
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ArcamFmjConfigEntry,
|
||||||
|
client: Client,
|
||||||
|
zone: int,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
|
name=f"Arcam FMJ zone {zone}",
|
||||||
|
)
|
||||||
|
self.client = client
|
||||||
|
self.state = State(client, zone)
|
||||||
|
self.update_in_progress = False
|
||||||
|
|
||||||
|
name = config_entry.title
|
||||||
|
unique_id = config_entry.unique_id or config_entry.entry_id
|
||||||
|
unique_id_device = unique_id
|
||||||
|
if zone != 1:
|
||||||
|
unique_id_device += f"-{zone}"
|
||||||
|
name += f" Zone {zone}"
|
||||||
|
|
||||||
|
self.device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, unique_id_device)},
|
||||||
|
manufacturer="Arcam",
|
||||||
|
model="Arcam FMJ AVR",
|
||||||
|
name=name,
|
||||||
|
)
|
||||||
|
self.zone_unique_id = f"{unique_id}-{zone}"
|
||||||
|
|
||||||
|
if zone != 1:
|
||||||
|
self.device_info["via_device"] = (DOMAIN, unique_id)
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> None:
|
||||||
|
"""Fetch data for manual refresh."""
|
||||||
|
try:
|
||||||
|
self.update_in_progress = True
|
||||||
|
await self.state.update()
|
||||||
|
except ConnectionFailed as err:
|
||||||
|
raise UpdateFailed(
|
||||||
|
f"Connection failed during update for zone {self.state.zn}"
|
||||||
|
) from err
|
||||||
|
finally:
|
||||||
|
self.update_in_progress = False
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_notify_packet(self, packet: ResponsePacket | AmxDuetResponse) -> None:
|
||||||
|
"""Packet callback to detect changes to state."""
|
||||||
|
if (
|
||||||
|
not isinstance(packet, ResponsePacket)
|
||||||
|
or packet.zn != self.state.zn
|
||||||
|
or self.update_in_progress
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.async_update_listeners()
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def async_monitor_client(self) -> AsyncGenerator[None]:
|
||||||
|
"""Monitor a client and state for changes while connected."""
|
||||||
|
async with self.state:
|
||||||
|
self.hass.async_create_task(self.async_refresh())
|
||||||
|
try:
|
||||||
|
with self.client.listen(self._async_notify_packet):
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self.hass.async_create_task(self.async_refresh())
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""Base entity for Arcam FMJ integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .coordinator import ArcamFmjCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
class ArcamFmjEntity(CoordinatorEntity[ArcamFmjCoordinator]):
|
||||||
|
"""Base entity for Arcam FMJ."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: ArcamFmjCoordinator,
|
||||||
|
description: EntityDescription | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._attr_device_info = coordinator.device_info
|
||||||
|
self._attr_entity_registry_enabled_default = coordinator.state.zn == 1
|
||||||
|
self._attr_unique_id = coordinator.zone_unique_id
|
||||||
|
if description is not None:
|
||||||
|
self._attr_unique_id = f"{self._attr_unique_id}-{description.key}"
|
||||||
|
self.entity_description = description
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return if entity is available."""
|
||||||
|
return super().available and self.coordinator.client.connected
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"binary_sensor": {
|
||||||
|
"incoming_video_interlaced": {
|
||||||
|
"default": "mdi:reorder-horizontal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sensor": {
|
||||||
|
"incoming_audio_config": {
|
||||||
|
"default": "mdi:surround-sound"
|
||||||
|
},
|
||||||
|
"incoming_audio_format": {
|
||||||
|
"default": "mdi:dolby"
|
||||||
|
},
|
||||||
|
"incoming_audio_sample_rate": {
|
||||||
|
"default": "mdi:waveform"
|
||||||
|
},
|
||||||
|
"incoming_video_aspect_ratio": {
|
||||||
|
"default": "mdi:aspect-ratio"
|
||||||
|
},
|
||||||
|
"incoming_video_colorspace": {
|
||||||
|
"default": "mdi:palette"
|
||||||
|
},
|
||||||
|
"incoming_video_horizontal_resolution": {
|
||||||
|
"default": "mdi:arrow-expand-horizontal"
|
||||||
|
},
|
||||||
|
"incoming_video_refresh_rate": {
|
||||||
|
"default": "mdi:animation"
|
||||||
|
},
|
||||||
|
"incoming_video_vertical_resolution": {
|
||||||
|
"default": "mdi:arrow-expand-vertical"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from arcam.fmj import ConnectionFailed, SourceCodes
|
from arcam.fmj import ConnectionFailed, SourceCodes
|
||||||
from arcam.fmj.state import State
|
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
BrowseError,
|
BrowseError,
|
||||||
@@ -20,20 +19,13 @@ from homeassistant.components.media_player import (
|
|||||||
MediaType,
|
MediaType,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_ENTITY_ID
|
from homeassistant.const import ATTR_ENTITY_ID
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import ArcamFmjConfigEntry
|
from .const import EVENT_TURN_ON
|
||||||
from .const import (
|
from .coordinator import ArcamFmjConfigEntry, ArcamFmjCoordinator
|
||||||
DOMAIN,
|
from .entity import ArcamFmjEntity
|
||||||
EVENT_TURN_ON,
|
|
||||||
SIGNAL_CLIENT_DATA,
|
|
||||||
SIGNAL_CLIENT_STARTED,
|
|
||||||
SIGNAL_CLIENT_STOPPED,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -44,19 +36,10 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the configuration entry."""
|
"""Set up the configuration entry."""
|
||||||
|
coordinators = config_entry.runtime_data.coordinators
|
||||||
client = config_entry.runtime_data
|
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[ArcamFmj(coordinators[zone]) for zone in (1, 2)],
|
||||||
ArcamFmj(
|
|
||||||
config_entry.title,
|
|
||||||
State(client, zone),
|
|
||||||
config_entry.unique_id or config_entry.entry_id,
|
|
||||||
)
|
|
||||||
for zone in (1, 2)
|
|
||||||
],
|
|
||||||
True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -77,21 +60,13 @@ def convert_exception[**_P, _R](
|
|||||||
return _convert_exception
|
return _convert_exception
|
||||||
|
|
||||||
|
|
||||||
class ArcamFmj(MediaPlayerEntity):
|
class ArcamFmj(ArcamFmjEntity, MediaPlayerEntity):
|
||||||
"""Representation of a media device."""
|
"""Representation of a media device."""
|
||||||
|
|
||||||
_attr_should_poll = False
|
def __init__(self, coordinator: ArcamFmjCoordinator) -> None:
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
device_name: str,
|
|
||||||
state: State,
|
|
||||||
uuid: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize device."""
|
"""Initialize device."""
|
||||||
self._state = state
|
super().__init__(coordinator)
|
||||||
self._attr_name = f"Zone {state.zn}"
|
self._state = coordinator.state
|
||||||
self._attr_supported_features = (
|
self._attr_supported_features = (
|
||||||
MediaPlayerEntityFeature.SELECT_SOURCE
|
MediaPlayerEntityFeature.SELECT_SOURCE
|
||||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||||
@@ -102,18 +77,8 @@ class ArcamFmj(MediaPlayerEntity):
|
|||||||
| MediaPlayerEntityFeature.TURN_OFF
|
| MediaPlayerEntityFeature.TURN_OFF
|
||||||
| MediaPlayerEntityFeature.TURN_ON
|
| MediaPlayerEntityFeature.TURN_ON
|
||||||
)
|
)
|
||||||
if state.zn == 1:
|
if self._state.zn == 1:
|
||||||
self._attr_supported_features |= MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
self._attr_supported_features |= MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
||||||
self._attr_unique_id = f"{uuid}-{state.zn}"
|
|
||||||
self._attr_entity_registry_enabled_default = state.zn == 1
|
|
||||||
self._attr_device_info = DeviceInfo(
|
|
||||||
identifiers={
|
|
||||||
(DOMAIN, uuid),
|
|
||||||
},
|
|
||||||
manufacturer="Arcam",
|
|
||||||
model="Arcam FMJ AVR",
|
|
||||||
name=device_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> MediaPlayerState:
|
def state(self) -> MediaPlayerState:
|
||||||
@@ -122,49 +87,6 @@ class ArcamFmj(MediaPlayerEntity):
|
|||||||
return MediaPlayerState.ON
|
return MediaPlayerState.ON
|
||||||
return MediaPlayerState.OFF
|
return MediaPlayerState.OFF
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
|
||||||
"""Once registered, add listener for events."""
|
|
||||||
await self._state.start()
|
|
||||||
try:
|
|
||||||
await self._state.update()
|
|
||||||
except ConnectionFailed as connection:
|
|
||||||
_LOGGER.debug("Connection lost during addition: %s", connection)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _data(host: str) -> None:
|
|
||||||
if host == self._state.client.host:
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _started(host: str) -> None:
|
|
||||||
if host == self._state.client.host:
|
|
||||||
self.async_schedule_update_ha_state(force_refresh=True)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _stopped(host: str) -> None:
|
|
||||||
if host == self._state.client.host:
|
|
||||||
self.async_schedule_update_ha_state(force_refresh=True)
|
|
||||||
|
|
||||||
self.async_on_remove(
|
|
||||||
async_dispatcher_connect(self.hass, SIGNAL_CLIENT_DATA, _data)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.async_on_remove(
|
|
||||||
async_dispatcher_connect(self.hass, SIGNAL_CLIENT_STARTED, _started)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.async_on_remove(
|
|
||||||
async_dispatcher_connect(self.hass, SIGNAL_CLIENT_STOPPED, _stopped)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
|
||||||
"""Force update of state."""
|
|
||||||
_LOGGER.debug("Update state %s", self.name)
|
|
||||||
try:
|
|
||||||
await self._state.update()
|
|
||||||
except ConnectionFailed as connection:
|
|
||||||
_LOGGER.debug("Connection lost during update: %s", connection)
|
|
||||||
|
|
||||||
@convert_exception
|
@convert_exception
|
||||||
async def async_mute_volume(self, mute: bool) -> None:
|
async def async_mute_volume(self, mute: bool) -> None:
|
||||||
"""Send mute command."""
|
"""Send mute command."""
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
"""Arcam sensors for incoming stream info."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from arcam.fmj import IncomingVideoAspectRatio, IncomingVideoColorspace
|
||||||
|
from arcam.fmj.state import IncomingAudioConfig, IncomingAudioFormat, State
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import EntityCategory, UnitOfFrequency
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import ArcamFmjConfigEntry
|
||||||
|
from .entity import ArcamFmjEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ArcamFmjSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Describes an Arcam FMJ sensor entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[State], int | float | str | None]
|
||||||
|
|
||||||
|
|
||||||
|
SENSORS: tuple[ArcamFmjSensorEntityDescription, ...] = (
|
||||||
|
ArcamFmjSensorEntityDescription(
|
||||||
|
key="incoming_video_horizontal_resolution",
|
||||||
|
translation_key="incoming_video_horizontal_resolution",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement="px",
|
||||||
|
suggested_display_precision=0,
|
||||||
|
value_fn=lambda state: (
|
||||||
|
vp.horizontal_resolution
|
||||||
|
if (vp := state.get_incoming_video_parameters()) is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ArcamFmjSensorEntityDescription(
|
||||||
|
key="incoming_video_vertical_resolution",
|
||||||
|
translation_key="incoming_video_vertical_resolution",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement="px",
|
||||||
|
suggested_display_precision=0,
|
||||||
|
value_fn=lambda state: (
|
||||||
|
vp.vertical_resolution
|
||||||
|
if (vp := state.get_incoming_video_parameters()) is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ArcamFmjSensorEntityDescription(
|
||||||
|
key="incoming_video_refresh_rate",
|
||||||
|
translation_key="incoming_video_refresh_rate",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
device_class=SensorDeviceClass.FREQUENCY,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
value_fn=lambda state: (
|
||||||
|
vp.refresh_rate
|
||||||
|
if (vp := state.get_incoming_video_parameters()) is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ArcamFmjSensorEntityDescription(
|
||||||
|
key="incoming_video_aspect_ratio",
|
||||||
|
translation_key="incoming_video_aspect_ratio",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=[member.name.lower() for member in IncomingVideoAspectRatio],
|
||||||
|
value_fn=lambda state: (
|
||||||
|
vp.aspect_ratio.name.lower()
|
||||||
|
if (vp := state.get_incoming_video_parameters()) is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ArcamFmjSensorEntityDescription(
|
||||||
|
key="incoming_video_colorspace",
|
||||||
|
translation_key="incoming_video_colorspace",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=[member.name.lower() for member in IncomingVideoColorspace],
|
||||||
|
value_fn=lambda state: (
|
||||||
|
vp.colorspace.name.lower()
|
||||||
|
if (vp := state.get_incoming_video_parameters()) is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ArcamFmjSensorEntityDescription(
|
||||||
|
key="incoming_audio_format",
|
||||||
|
translation_key="incoming_audio_format",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=[member.name.lower() for member in IncomingAudioFormat],
|
||||||
|
value_fn=lambda state: (
|
||||||
|
result.name.lower()
|
||||||
|
if (result := state.get_incoming_audio_format()[0]) is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ArcamFmjSensorEntityDescription(
|
||||||
|
key="incoming_audio_config",
|
||||||
|
translation_key="incoming_audio_config",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=[member.name.lower() for member in IncomingAudioConfig],
|
||||||
|
value_fn=lambda state: (
|
||||||
|
result.name.lower()
|
||||||
|
if (result := state.get_incoming_audio_format()[1]) is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ArcamFmjSensorEntityDescription(
|
||||||
|
key="incoming_audio_sample_rate",
|
||||||
|
translation_key="incoming_audio_sample_rate",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
device_class=SensorDeviceClass.FREQUENCY,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
value_fn=lambda state: (
|
||||||
|
None
|
||||||
|
if (sample_rate := state.get_incoming_audio_sample_rate()) == 0
|
||||||
|
else sample_rate
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ArcamFmjConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Arcam FMJ sensors from a config entry."""
|
||||||
|
coordinators = config_entry.runtime_data.coordinators
|
||||||
|
|
||||||
|
entities: list[ArcamFmjSensorEntity] = []
|
||||||
|
for coordinator in coordinators.values():
|
||||||
|
entities.extend(
|
||||||
|
ArcamFmjSensorEntity(coordinator, description) for description in SENSORS
|
||||||
|
)
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class ArcamFmjSensorEntity(ArcamFmjEntity, SensorEntity):
|
||||||
|
"""Representation of an Arcam FMJ sensor."""
|
||||||
|
|
||||||
|
entity_description: ArcamFmjSensorEntityDescription
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> int | float | str | None:
|
||||||
|
"""Return the sensor value."""
|
||||||
|
return self.entity_description.value_fn(self.coordinator.state)
|
||||||
@@ -23,5 +23,121 @@
|
|||||||
"trigger_type": {
|
"trigger_type": {
|
||||||
"turn_on": "{entity_name} was requested to turn on"
|
"turn_on": "{entity_name} was requested to turn on"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"binary_sensor": {
|
||||||
|
"incoming_video_interlaced": {
|
||||||
|
"name": "Incoming video interlaced"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sensor": {
|
||||||
|
"incoming_audio_config": {
|
||||||
|
"name": "Incoming audio configuration",
|
||||||
|
"state": {
|
||||||
|
"auro_10_1": "Auro 10.1",
|
||||||
|
"auro_11_1": "Auro 11.1",
|
||||||
|
"auro_13_1": "Auro 13.1",
|
||||||
|
"auro_2_2_2": "Auro 2.2.2",
|
||||||
|
"auro_5_0": "Auro 5.0",
|
||||||
|
"auro_5_1": "Auro 5.1",
|
||||||
|
"auro_8_0": "Auro 8.0",
|
||||||
|
"auro_9_1": "Auro 9.1",
|
||||||
|
"auro_quad": "Auro quad",
|
||||||
|
"dual_mono": "Dual mono",
|
||||||
|
"dual_mono_lfe": "Dual mono + LFE",
|
||||||
|
"mono": "Mono",
|
||||||
|
"mono_lfe": "Mono + LFE",
|
||||||
|
"stereo_center": "Stereo center",
|
||||||
|
"stereo_center_lfe": "Stereo center + LFE",
|
||||||
|
"stereo_center_surr_lr": "Stereo center surround L/R",
|
||||||
|
"stereo_center_surr_lr_back_lr": "Stereo center surround L/R back L/R",
|
||||||
|
"stereo_center_surr_lr_back_lr_lfe": "Stereo center surround L/R back L/R + LFE",
|
||||||
|
"stereo_center_surr_lr_back_matrix": "Stereo center surround L/R back matrix",
|
||||||
|
"stereo_center_surr_lr_back_matrix_lfe": "Stereo center surround L/R back matrix + LFE",
|
||||||
|
"stereo_center_surr_lr_back_mono": "Stereo center surround L/R back mono",
|
||||||
|
"stereo_center_surr_lr_back_mono_lfe": "Stereo center surround L/R back mono + LFE",
|
||||||
|
"stereo_center_surr_lr_lfe": "Stereo center surround L/R + LFE",
|
||||||
|
"stereo_center_surr_mono": "Stereo center surround mono",
|
||||||
|
"stereo_center_surr_mono_lfe": "Stereo center surround mono + LFE",
|
||||||
|
"stereo_downmix": "Stereo downmix",
|
||||||
|
"stereo_downmix_lfe": "Stereo downmix + LFE",
|
||||||
|
"stereo_lfe": "Stereo + LFE",
|
||||||
|
"stereo_only": "Stereo only",
|
||||||
|
"stereo_only_lo_ro": "Stereo only Lo/Ro",
|
||||||
|
"stereo_only_lo_ro_lfe": "Stereo only Lo/Ro + LFE",
|
||||||
|
"stereo_surr_lr": "Stereo surround L/R",
|
||||||
|
"stereo_surr_lr_back_lr": "Stereo surround L/R back L/R",
|
||||||
|
"stereo_surr_lr_back_lr_lfe": "Stereo surround L/R back L/R + LFE",
|
||||||
|
"stereo_surr_lr_back_matrix": "Stereo surround L/R back matrix",
|
||||||
|
"stereo_surr_lr_back_matrix_lfe": "Stereo surround L/R back matrix + LFE",
|
||||||
|
"stereo_surr_lr_back_mono": "Stereo surround L/R back mono",
|
||||||
|
"stereo_surr_lr_back_mono_lfe": "Stereo surround L/R back mono + LFE",
|
||||||
|
"stereo_surr_lr_lfe": "Stereo surround L/R + LFE",
|
||||||
|
"stereo_surr_mono": "Stereo surround mono",
|
||||||
|
"stereo_surr_mono_lfe": "Stereo surround mono + LFE",
|
||||||
|
"undetected": "Undetected",
|
||||||
|
"unknown": "Unknown"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"incoming_audio_format": {
|
||||||
|
"name": "Incoming audio format",
|
||||||
|
"state": {
|
||||||
|
"analogue_direct": "Analogue direct",
|
||||||
|
"auro_3d": "Auro-3D",
|
||||||
|
"dolby_atmos": "Dolby Atmos",
|
||||||
|
"dolby_digital": "Dolby Digital",
|
||||||
|
"dolby_digital_ex": "Dolby Digital EX",
|
||||||
|
"dolby_digital_plus": "Dolby Digital Plus",
|
||||||
|
"dolby_digital_surround": "Dolby Digital Surround",
|
||||||
|
"dolby_digital_true_hd": "Dolby TrueHD",
|
||||||
|
"dts": "DTS",
|
||||||
|
"dts_96_24": "DTS 96/24",
|
||||||
|
"dts_core": "DTS Core",
|
||||||
|
"dts_es_discrete": "DTS-ES Discrete",
|
||||||
|
"dts_es_discrete_96_24": "DTS-ES Discrete 96/24",
|
||||||
|
"dts_es_matrix": "DTS-ES Matrix",
|
||||||
|
"dts_es_matrix_96_24": "DTS-ES Matrix 96/24",
|
||||||
|
"dts_hd_high_res_audio": "DTS-HD High Resolution Audio",
|
||||||
|
"dts_hd_master_audio": "DTS-HD Master Audio",
|
||||||
|
"dts_low_bit_rate": "DTS Low Bit Rate",
|
||||||
|
"dts_x": "DTS:X",
|
||||||
|
"imax_enhanced": "IMAX Enhanced",
|
||||||
|
"pcm": "PCM",
|
||||||
|
"pcm_zero": "PCM zero",
|
||||||
|
"undetected": "Undetected",
|
||||||
|
"unsupported": "Unsupported"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"incoming_audio_sample_rate": {
|
||||||
|
"name": "Incoming audio sample rate"
|
||||||
|
},
|
||||||
|
"incoming_video_aspect_ratio": {
|
||||||
|
"name": "Incoming video aspect ratio",
|
||||||
|
"state": {
|
||||||
|
"aspect_16_9": "16:9",
|
||||||
|
"aspect_4_3": "4:3",
|
||||||
|
"undefined": "Undefined"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"incoming_video_colorspace": {
|
||||||
|
"name": "Incoming video colorspace",
|
||||||
|
"state": {
|
||||||
|
"dolby_vision": "Dolby Vision",
|
||||||
|
"hdr10": "HDR10",
|
||||||
|
"hdr10_plus": "HDR10+",
|
||||||
|
"hlg": "HLG",
|
||||||
|
"normal": "Normal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"incoming_video_horizontal_resolution": {
|
||||||
|
"name": "Incoming video horizontal resolution"
|
||||||
|
},
|
||||||
|
"incoming_video_refresh_rate": {
|
||||||
|
"name": "Incoming video refresh rate"
|
||||||
|
},
|
||||||
|
"incoming_video_vertical_resolution": {
|
||||||
|
"name": "Incoming video vertical resolution"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
"""The Arris TG2492LG component."""
|
"""The Arris TG2492LG integration."""
|
||||||
|
|||||||
@@ -137,5 +137,4 @@ async def async_pipeline_from_audio_stream(
|
|||||||
audio_settings=audio_settings or AudioSettings(),
|
audio_settings=audio_settings or AudioSettings(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await pipeline_input.validate()
|
await pipeline_input.execute(validate=True)
|
||||||
await pipeline_input.execute()
|
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
"""Assist pipeline errors."""
|
"""Assist pipeline errors."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .pipeline import PipelineStage
|
||||||
|
|
||||||
|
|
||||||
class PipelineError(HomeAssistantError):
|
class PipelineError(HomeAssistantError):
|
||||||
"""Base class for pipeline errors."""
|
"""Base class for pipeline errors."""
|
||||||
@@ -55,3 +62,25 @@ class IntentRecognitionError(PipelineError):
|
|||||||
|
|
||||||
class TextToSpeechError(PipelineError):
|
class TextToSpeechError(PipelineError):
|
||||||
"""Error in text-to-speech portion of pipeline."""
|
"""Error in text-to-speech portion of pipeline."""
|
||||||
|
|
||||||
|
|
||||||
|
class PipelineRunValidationError(PipelineError):
|
||||||
|
"""Error when a pipeline run is not valid."""
|
||||||
|
|
||||||
|
def __init__(self, message: str) -> None:
|
||||||
|
"""Set error message."""
|
||||||
|
super().__init__("validation-error", message)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidPipelineStagesError(PipelineRunValidationError):
|
||||||
|
"""Error when given an invalid combination of start/end stages."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
start_stage: PipelineStage,
|
||||||
|
end_stage: PipelineStage,
|
||||||
|
) -> None:
|
||||||
|
"""Set error message."""
|
||||||
|
super().__init__(
|
||||||
|
f"Invalid stage combination: start={start_stage}, end={end_stage}"
|
||||||
|
)
|
||||||
|
|||||||
@@ -73,8 +73,10 @@ from .const import (
|
|||||||
from .error import (
|
from .error import (
|
||||||
DuplicateWakeUpDetectedError,
|
DuplicateWakeUpDetectedError,
|
||||||
IntentRecognitionError,
|
IntentRecognitionError,
|
||||||
|
InvalidPipelineStagesError,
|
||||||
PipelineError,
|
PipelineError,
|
||||||
PipelineNotFound,
|
PipelineNotFound,
|
||||||
|
PipelineRunValidationError,
|
||||||
SpeechToTextError,
|
SpeechToTextError,
|
||||||
TextToSpeechError,
|
TextToSpeechError,
|
||||||
WakeWordDetectionAborted,
|
WakeWordDetectionAborted,
|
||||||
@@ -492,24 +494,6 @@ PIPELINE_STAGE_ORDER = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PipelineRunValidationError(Exception):
|
|
||||||
"""Error when a pipeline run is not valid."""
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidPipelineStagesError(PipelineRunValidationError):
|
|
||||||
"""Error when given an invalid combination of start/end stages."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
start_stage: PipelineStage,
|
|
||||||
end_stage: PipelineStage,
|
|
||||||
) -> None:
|
|
||||||
"""Set error message."""
|
|
||||||
super().__init__(
|
|
||||||
f"Invalid stage combination: start={start_stage}, end={end_stage}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class WakeWordSettings:
|
class WakeWordSettings:
|
||||||
"""Settings for wake word detection."""
|
"""Settings for wake word detection."""
|
||||||
@@ -662,7 +646,8 @@ class PipelineRun:
|
|||||||
"""Emit run start event."""
|
"""Emit run start event."""
|
||||||
self._device_id = device_id
|
self._device_id = device_id
|
||||||
self._satellite_id = satellite_id
|
self._satellite_id = satellite_id
|
||||||
self._start_debug_recording_thread()
|
if self.start_stage in (PipelineStage.WAKE_WORD, PipelineStage.STT):
|
||||||
|
self._start_debug_recording_thread()
|
||||||
|
|
||||||
data: dict[str, Any] = {
|
data: dict[str, Any] = {
|
||||||
"pipeline": self.pipeline.id,
|
"pipeline": self.pipeline.id,
|
||||||
@@ -1504,9 +1489,7 @@ class PipelineRun:
|
|||||||
|
|
||||||
def _start_debug_recording_thread(self) -> None:
|
def _start_debug_recording_thread(self) -> None:
|
||||||
"""Start thread to record wake/stt audio if debug_recording_dir is set."""
|
"""Start thread to record wake/stt audio if debug_recording_dir is set."""
|
||||||
if self.debug_recording_thread is not None:
|
assert self.debug_recording_thread is None
|
||||||
# Already started
|
|
||||||
return
|
|
||||||
|
|
||||||
# Directory to save audio for each pipeline run.
|
# Directory to save audio for each pipeline run.
|
||||||
# Configured in YAML for assist_pipeline.
|
# Configured in YAML for assist_pipeline.
|
||||||
@@ -1681,26 +1664,39 @@ class PipelineInput:
|
|||||||
satellite_id: str | None = None
|
satellite_id: str | None = None
|
||||||
"""Identifier of the satellite that is processing the input/output of the pipeline."""
|
"""Identifier of the satellite that is processing the input/output of the pipeline."""
|
||||||
|
|
||||||
async def execute(self) -> None:
|
async def execute(self, validate: bool = False) -> None:
|
||||||
"""Run pipeline."""
|
"""Run pipeline."""
|
||||||
|
validation_error: PipelineError | None = None
|
||||||
|
if validate:
|
||||||
|
try:
|
||||||
|
await self.validate()
|
||||||
|
except PipelineError as err:
|
||||||
|
validation_error = err
|
||||||
|
|
||||||
self.run.start(
|
self.run.start(
|
||||||
conversation_id=self.session.conversation_id,
|
conversation_id=self.session.conversation_id,
|
||||||
device_id=self.device_id,
|
device_id=self.device_id,
|
||||||
satellite_id=self.satellite_id,
|
satellite_id=self.satellite_id,
|
||||||
)
|
)
|
||||||
current_stage: PipelineStage | None = self.run.start_stage
|
current_stage: PipelineStage | None = self.run.start_stage
|
||||||
stt_audio_buffer: list[EnhancedAudioChunk] = []
|
|
||||||
stt_processed_stream: AsyncIterable[EnhancedAudioChunk] | None = None
|
|
||||||
|
|
||||||
if self.stt_stream is not None:
|
|
||||||
if self.run.audio_settings.needs_processor:
|
|
||||||
# VAD/noise suppression/auto gain/volume
|
|
||||||
stt_processed_stream = self.run.process_enhance_audio(self.stt_stream)
|
|
||||||
else:
|
|
||||||
# Volume multiplier only
|
|
||||||
stt_processed_stream = self.run.process_volume_only(self.stt_stream)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if validation_error is not None:
|
||||||
|
raise validation_error
|
||||||
|
|
||||||
|
stt_audio_buffer: list[EnhancedAudioChunk] = []
|
||||||
|
stt_processed_stream: AsyncIterable[EnhancedAudioChunk] | None = None
|
||||||
|
|
||||||
|
if self.stt_stream is not None:
|
||||||
|
if self.run.audio_settings.needs_processor:
|
||||||
|
# VAD/noise suppression/auto gain/volume
|
||||||
|
stt_processed_stream = self.run.process_enhance_audio(
|
||||||
|
self.stt_stream
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Volume multiplier only
|
||||||
|
stt_processed_stream = self.run.process_volume_only(self.stt_stream)
|
||||||
|
|
||||||
if current_stage == PipelineStage.WAKE_WORD:
|
if current_stage == PipelineStage.WAKE_WORD:
|
||||||
# wake-word-detection
|
# wake-word-detection
|
||||||
assert stt_processed_stream is not None
|
assert stt_processed_stream is not None
|
||||||
|
|||||||
@@ -78,19 +78,13 @@ class AssistPipelineSelect(SelectEntity, restore_state.RestoreEntity):
|
|||||||
index: int = 0,
|
index: int = 0,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a pipeline selector."""
|
"""Initialize a pipeline selector."""
|
||||||
if index < 1:
|
if index >= 1:
|
||||||
# Keep compatibility
|
self.entity_description = replace(
|
||||||
key_suffix = ""
|
self.entity_description,
|
||||||
placeholder = ""
|
key=f"pipeline_{index + 1}",
|
||||||
else:
|
translation_key="pipeline_n",
|
||||||
key_suffix = f"_{index + 1}"
|
translation_placeholders={"index": str(index + 1)},
|
||||||
placeholder = f" {index + 1}"
|
)
|
||||||
|
|
||||||
self.entity_description = replace(
|
|
||||||
self.entity_description,
|
|
||||||
key=f"pipeline{key_suffix}",
|
|
||||||
translation_placeholders={"index": placeholder},
|
|
||||||
)
|
|
||||||
|
|
||||||
self._domain = domain
|
self._domain = domain
|
||||||
self._unique_id_prefix = unique_id_prefix
|
self._unique_id_prefix = unique_id_prefix
|
||||||
|
|||||||
@@ -7,11 +7,17 @@
|
|||||||
},
|
},
|
||||||
"select": {
|
"select": {
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"name": "Assistant{index}",
|
"name": "Assistant",
|
||||||
"state": {
|
"state": {
|
||||||
"preferred": "Preferred"
|
"preferred": "Preferred"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pipeline_n": {
|
||||||
|
"name": "Assistant {index}",
|
||||||
|
"state": {
|
||||||
|
"preferred": "[%key:component::assist_pipeline::entity::select::pipeline::state::preferred%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
"vad_sensitivity": {
|
"vad_sensitivity": {
|
||||||
"name": "Finished speaking detection",
|
"name": "Finished speaking detection",
|
||||||
"state": {
|
"state": {
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
"condition_behavior_description": "How the state should match on the targeted Assist satellites.",
|
"condition_behavior_name": "Condition passes if",
|
||||||
"condition_behavior_name": "Behavior",
|
"trigger_behavior_name": "Trigger when"
|
||||||
"trigger_behavior_description": "The behavior of the targeted Assist satellites to trigger on.",
|
|
||||||
"trigger_behavior_name": "Behavior"
|
|
||||||
},
|
},
|
||||||
"conditions": {
|
"conditions": {
|
||||||
"is_idle": {
|
"is_idle": {
|
||||||
"description": "Tests if one or more Assist satellites are idle.",
|
"description": "Tests if one or more Assist satellites are idle.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -20,7 +17,6 @@
|
|||||||
"description": "Tests if one or more Assist satellites are listening.",
|
"description": "Tests if one or more Assist satellites are listening.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -30,7 +26,6 @@
|
|||||||
"description": "Tests if one or more Assist satellites are processing.",
|
"description": "Tests if one or more Assist satellites are processing.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -40,7 +35,6 @@
|
|||||||
"description": "Tests if one or more Assist satellites are responding.",
|
"description": "Tests if one or more Assist satellites are responding.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
|
|
||||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -165,7 +159,6 @@
|
|||||||
"description": "Triggers after one or more voice assistant satellites become idle after having processed a command.",
|
"description": "Triggers after one or more voice assistant satellites become idle after having processed a command.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -175,7 +168,6 @@
|
|||||||
"description": "Triggers after one or more voice assistant satellites start listening for a command from someone.",
|
"description": "Triggers after one or more voice assistant satellites start listening for a command from someone.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -185,7 +177,6 @@
|
|||||||
"description": "Triggers after one or more voice assistant satellites start processing a command after having heard it.",
|
"description": "Triggers after one or more voice assistant satellites start processing a command after having heard it.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -195,7 +186,6 @@
|
|||||||
"description": "Triggers after one or more voice assistant satellites start responding to a command after having processed it, or start announcing something.",
|
"description": "Triggers after one or more voice assistant satellites start responding to a command after having processed it, or start announcing something.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"description": "[%key:component::assist_satellite::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from aiohttp import ClientResponseError
|
from aiohttp import ClientError
|
||||||
from yalexs.exceptions import AugustApiAIOHTTPError
|
from yalexs.exceptions import AugustApiAIOHTTPError
|
||||||
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||||
from yalexs.manager.gateway import Config as YaleXSConfig
|
from yalexs.manager.gateway import Config as YaleXSConfig
|
||||||
@@ -13,7 +13,12 @@ from yalexs.manager.gateway import Config as YaleXSConfig
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import (
|
||||||
|
ConfigEntryAuthFailed,
|
||||||
|
ConfigEntryNotReady,
|
||||||
|
OAuth2TokenRequestError,
|
||||||
|
OAuth2TokenRequestReauthError,
|
||||||
|
)
|
||||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||||
ImplementationUnavailableError,
|
ImplementationUnavailableError,
|
||||||
@@ -45,11 +50,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bo
|
|||||||
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
|
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
|
||||||
try:
|
try:
|
||||||
await async_setup_august(hass, entry, august_gateway)
|
await async_setup_august(hass, entry, august_gateway)
|
||||||
|
except OAuth2TokenRequestReauthError as err:
|
||||||
|
raise ConfigEntryAuthFailed from err
|
||||||
except (RequireValidation, InvalidAuth) as err:
|
except (RequireValidation, InvalidAuth) as err:
|
||||||
raise ConfigEntryAuthFailed from err
|
raise ConfigEntryAuthFailed from err
|
||||||
except TimeoutError as err:
|
except TimeoutError as err:
|
||||||
raise ConfigEntryNotReady("Timed out connecting to august api") from err
|
raise ConfigEntryNotReady("Timed out connecting to august api") from err
|
||||||
except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err:
|
except (
|
||||||
|
AugustApiAIOHTTPError,
|
||||||
|
OAuth2TokenRequestError,
|
||||||
|
ClientError,
|
||||||
|
CannotConnect,
|
||||||
|
) as err:
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
return True
|
return True
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user