mirror of
https://github.com/home-assistant/core.git
synced 2026-04-29 10:23:46 +02:00
Compare commits
905 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 | |||
| 2c47e83342 | |||
| e3c6a2184d | |||
| 0ba0829350 | |||
| 678048e681 | |||
| 743eeeae53 | |||
| 46555c6d9a | |||
| dbaca0a723 | |||
| 9bb2959029 | |||
| 0304781fa9 | |||
| e081d28aa4 | |||
| 34aa28c72f | |||
| cfa2946db8 | |||
| 1b0779347c | |||
| 93a281e7af | |||
| 6b32e27fd3 | |||
| 79928a8c7c | |||
| 9146518e13 | |||
| e9c5172f43 | |||
| cce21ad4b9 | |||
| 10ec02ca3c | |||
| bdf54491e5 | |||
| 0b05d34238 | |||
| 4c69a1c5f7 | |||
| 6f1f56dcaa | |||
| d0b9991232 | |||
| aacf39be8a | |||
| bf055da82c | |||
| 0fb118bcd9 | |||
| 954ef7d1f5 | |||
| b091299320 | |||
| 52483e18b2 | |||
| 57e8683ed7 | |||
| 67faace978 | |||
| e4be64fcb1 | |||
| 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 | |||
| 2f9faa53a1 | |||
| 718607a758 | |||
| 3789156559 | |||
| 042ce6f2de | |||
| 0a5908002f | |||
| 3a5f71e10a | |||
| 04e4b05ab0 | |||
| 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 | |||
| 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 |
@@ -5,14 +5,6 @@ description: Review a GitHub pull request and provide feedback comments. Use whe
|
|||||||
|
|
||||||
# Review GitHub Pull Request
|
# Review GitHub Pull Request
|
||||||
|
|
||||||
## Preparation:
|
|
||||||
- Check if the local commit matches the last one in the PR. If not, checkout the PR locally using 'gh pr checkout'.
|
|
||||||
- CRITICAL: If 'gh pr checkout' fails for ANY reason, you MUST immediately STOP.
|
|
||||||
- Do NOT attempt any workarounds.
|
|
||||||
- Do NOT proceed with the review.
|
|
||||||
- ALERT about the failure and WAIT for instructions.
|
|
||||||
- This is a hard requirement - no exceptions.
|
|
||||||
|
|
||||||
## Follow these steps:
|
## Follow these steps:
|
||||||
1. Use 'gh pr view' to get the PR details and description.
|
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.
|
2. Use 'gh pr diff' to see all the changes in the PR.
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
<!-- Automatically generated by gen_copilot_instructions.py, do not edit -->
|
<!-- 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
|
||||||
|
|
||||||
This repository contains the core of Home Assistant, a Python 3 based home automation application.
|
This repository contains the core of Home Assistant, a Python 3 based home automation application.
|
||||||
|
|||||||
+283
-287
@@ -57,10 +57,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
type: ${{ env.BUILD_TYPE }}
|
type: ${{ env.BUILD_TYPE }}
|
||||||
|
|
||||||
# - name: Verify version
|
- name: Verify version
|
||||||
# uses: home-assistant/actions/helpers/verify-version@master # zizmor: ignore[unpinned-uses]
|
uses: home-assistant/actions/helpers/verify-version@master # zizmor: ignore[unpinned-uses]
|
||||||
# with:
|
with:
|
||||||
# ignore-dev: true
|
ignore-dev: true
|
||||||
|
|
||||||
- name: Fail if translations files are checked in
|
- name: Fail if translations files are checked in
|
||||||
run: |
|
run: |
|
||||||
@@ -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@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
|
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@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
|
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
|
||||||
@@ -182,7 +182,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -224,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
|
||||||
@@ -248,10 +247,6 @@ 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
|
||||||
@@ -290,278 +285,279 @@ jobs:
|
|||||||
${{ steps.tags.outputs.extra_tags }}
|
${{ steps.tags.outputs.extra_tags }}
|
||||||
push: true
|
push: true
|
||||||
version: ${{ needs.init.outputs.version }}
|
version: ${{ needs.init.outputs.version }}
|
||||||
# publish_ha:
|
|
||||||
# name: Publish version files
|
publish_ha:
|
||||||
# environment: ${{ needs.init.outputs.channel }}
|
name: Publish version files
|
||||||
# if: github.repository_owner == 'home-assistant'
|
environment: ${{ needs.init.outputs.channel }}
|
||||||
# needs: ["init", "build_machine"]
|
if: github.repository_owner == 'home-assistant'
|
||||||
# runs-on: ubuntu-latest
|
needs: ["init", "build_machine"]
|
||||||
# permissions:
|
runs-on: ubuntu-latest
|
||||||
# contents: read
|
permissions:
|
||||||
# steps:
|
contents: read
|
||||||
# - name: Checkout the repository
|
steps:
|
||||||
# uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- name: Checkout the repository
|
||||||
# with:
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
# persist-credentials: false
|
with:
|
||||||
#
|
persist-credentials: false
|
||||||
# - name: Initialize git
|
|
||||||
# uses: home-assistant/actions/helpers/git-init@master # zizmor: ignore[unpinned-uses]
|
- name: Initialize git
|
||||||
# with:
|
uses: home-assistant/actions/helpers/git-init@master # zizmor: ignore[unpinned-uses]
|
||||||
# name: ${{ secrets.GIT_NAME }}
|
with:
|
||||||
# email: ${{ secrets.GIT_EMAIL }}
|
name: ${{ secrets.GIT_NAME }}
|
||||||
# token: ${{ secrets.GIT_TOKEN }}
|
email: ${{ secrets.GIT_EMAIL }}
|
||||||
#
|
token: ${{ secrets.GIT_TOKEN }}
|
||||||
# - name: Update version file
|
|
||||||
# uses: home-assistant/actions/helpers/version-push@master # zizmor: ignore[unpinned-uses]
|
- name: Update version file
|
||||||
# with:
|
uses: home-assistant/actions/helpers/version-push@master # zizmor: ignore[unpinned-uses]
|
||||||
# key: "homeassistant[]"
|
with:
|
||||||
# key-description: "Home Assistant Core"
|
key: "homeassistant[]"
|
||||||
# version: ${{ needs.init.outputs.version }}
|
key-description: "Home Assistant Core"
|
||||||
# channel: ${{ needs.init.outputs.channel }}
|
version: ${{ needs.init.outputs.version }}
|
||||||
# exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
|
channel: ${{ needs.init.outputs.channel }}
|
||||||
#
|
exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
|
||||||
# - name: Update version file (stable -> beta)
|
|
||||||
# if: needs.init.outputs.channel == 'stable'
|
- name: Update version file (stable -> beta)
|
||||||
# uses: home-assistant/actions/helpers/version-push@master # zizmor: ignore[unpinned-uses]
|
if: needs.init.outputs.channel == 'stable'
|
||||||
# with:
|
uses: home-assistant/actions/helpers/version-push@master # zizmor: ignore[unpinned-uses]
|
||||||
# key: "homeassistant[]"
|
with:
|
||||||
# key-description: "Home Assistant Core"
|
key: "homeassistant[]"
|
||||||
# version: ${{ needs.init.outputs.version }}
|
key-description: "Home Assistant Core"
|
||||||
# channel: beta
|
version: ${{ needs.init.outputs.version }}
|
||||||
# exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
|
channel: beta
|
||||||
#
|
exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
|
||||||
# publish_container:
|
|
||||||
# name: Publish meta container for ${{ matrix.registry }}
|
publish_container:
|
||||||
# environment: ${{ needs.init.outputs.channel }}
|
name: Publish meta container for ${{ matrix.registry }}
|
||||||
# if: github.repository_owner == 'home-assistant'
|
environment: ${{ needs.init.outputs.channel }}
|
||||||
# needs: ["init", "build_base"]
|
if: github.repository_owner == 'home-assistant'
|
||||||
# runs-on: ubuntu-latest
|
needs: ["init", "build_base"]
|
||||||
# permissions:
|
runs-on: ubuntu-latest
|
||||||
# contents: read # To check out the repository
|
permissions:
|
||||||
# packages: write # To push to GHCR
|
contents: read # To check out the repository
|
||||||
# id-token: write # For cosign signing
|
packages: write # To push to GHCR
|
||||||
# strategy:
|
id-token: write # For cosign signing
|
||||||
# fail-fast: false
|
strategy:
|
||||||
# matrix:
|
fail-fast: false
|
||||||
# registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
matrix:
|
||||||
# steps:
|
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||||
# - name: Install Cosign
|
steps:
|
||||||
# uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
|
- name: Install Cosign
|
||||||
# with:
|
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
|
||||||
# cosign-release: "v2.5.3"
|
with:
|
||||||
#
|
cosign-release: "v2.5.3"
|
||||||
# - name: Login to DockerHub
|
|
||||||
# if: matrix.registry == 'docker.io/homeassistant'
|
- name: Login to DockerHub
|
||||||
# uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
if: matrix.registry == 'docker.io/homeassistant'
|
||||||
# with:
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
# username: ${{ secrets.DOCKERHUB_USERNAME }}
|
with:
|
||||||
# password: ${{ secrets.DOCKERHUB_TOKEN }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
#
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
# - name: Login to GitHub Container Registry
|
|
||||||
# uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
- name: Login to GitHub Container Registry
|
||||||
# with:
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
# registry: ghcr.io
|
with:
|
||||||
# username: ${{ github.repository_owner }}
|
registry: ghcr.io
|
||||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
username: ${{ github.repository_owner }}
|
||||||
#
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
# - name: Verify architecture image signatures
|
|
||||||
# shell: bash
|
- name: Verify architecture image signatures
|
||||||
# env:
|
shell: bash
|
||||||
# ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
env:
|
||||||
# VERSION: ${{ needs.init.outputs.version }}
|
ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
||||||
# run: |
|
VERSION: ${{ needs.init.outputs.version }}
|
||||||
# ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]')
|
run: |
|
||||||
# for arch in $ARCHS; do
|
ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]')
|
||||||
# echo "Verifying ${arch} image signature..."
|
for arch in $ARCHS; do
|
||||||
# cosign verify \
|
echo "Verifying ${arch} image signature..."
|
||||||
# --certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
cosign verify \
|
||||||
# --certificate-identity-regexp https://github.com/home-assistant/core/.* \
|
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||||
# "ghcr.io/home-assistant/${arch}-homeassistant:${VERSION}"
|
--certificate-identity-regexp https://github.com/home-assistant/core/.* \
|
||||||
# done
|
"ghcr.io/home-assistant/${arch}-homeassistant:${VERSION}"
|
||||||
# echo "✓ All images verified successfully"
|
done
|
||||||
#
|
echo "✓ All images verified successfully"
|
||||||
# # Generate all Docker tags based on version string
|
|
||||||
# # Version format: YYYY.MM.PATCH, YYYY.MM.PATCHbN (beta), or YYYY.MM.PATCH.devYYYYMMDDHHMM (dev)
|
# Generate all Docker tags based on version string
|
||||||
# # Examples:
|
# Version format: YYYY.MM.PATCH, YYYY.MM.PATCHbN (beta), or YYYY.MM.PATCH.devYYYYMMDDHHMM (dev)
|
||||||
# # 2025.12.1 (stable) -> tags: 2025.12.1, 2025.12, stable, latest, beta, rc
|
# Examples:
|
||||||
# # 2025.12.0b3 (beta) -> tags: 2025.12.0b3, beta, rc
|
# 2025.12.1 (stable) -> tags: 2025.12.1, 2025.12, stable, latest, beta, rc
|
||||||
# # 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
|
# 2025.12.0b3 (beta) -> tags: 2025.12.0b3, beta, rc
|
||||||
# - name: Generate Docker metadata
|
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
|
||||||
# id: meta
|
- name: Generate Docker metadata
|
||||||
# uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
id: meta
|
||||||
# with:
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||||
# images: ${{ matrix.registry }}/home-assistant
|
with:
|
||||||
# sep-tags: ","
|
images: ${{ matrix.registry }}/home-assistant
|
||||||
# tags: |
|
sep-tags: ","
|
||||||
# type=raw,value=${{ needs.init.outputs.version }},priority=9999
|
tags: |
|
||||||
# type=raw,value=dev,enable=${{ contains(needs.init.outputs.version, 'd') }}
|
type=raw,value=${{ needs.init.outputs.version }},priority=9999
|
||||||
# type=raw,value=beta,enable=${{ !contains(needs.init.outputs.version, 'd') }}
|
type=raw,value=dev,enable=${{ contains(needs.init.outputs.version, 'd') }}
|
||||||
# type=raw,value=rc,enable=${{ !contains(needs.init.outputs.version, 'd') }}
|
type=raw,value=beta,enable=${{ !contains(needs.init.outputs.version, 'd') }}
|
||||||
# type=raw,value=stable,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
type=raw,value=rc,enable=${{ !contains(needs.init.outputs.version, 'd') }}
|
||||||
# type=raw,value=latest,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
type=raw,value=stable,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') }}
|
type=raw,value=latest,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
|
|
||||||
# uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v3.7.1
|
- name: Set up Docker Buildx
|
||||||
#
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v3.7.1
|
||||||
# - name: Copy architecture images to DockerHub
|
|
||||||
# if: matrix.registry == 'docker.io/homeassistant'
|
- name: Copy architecture images to DockerHub
|
||||||
# shell: bash
|
if: matrix.registry == 'docker.io/homeassistant'
|
||||||
# env:
|
shell: bash
|
||||||
# ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
env:
|
||||||
# VERSION: ${{ needs.init.outputs.version }}
|
ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
||||||
# run: |
|
VERSION: ${{ needs.init.outputs.version }}
|
||||||
# # Use imagetools to copy image blobs directly between registries
|
run: |
|
||||||
# # This preserves provenance/attestations and seems to be much faster than pull/push
|
# Use imagetools to copy image blobs directly between registries
|
||||||
# ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]')
|
# This preserves provenance/attestations and seems to be much faster than pull/push
|
||||||
# for arch in $ARCHS; do
|
ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]')
|
||||||
# echo "Copying ${arch} image to DockerHub..."
|
for arch in $ARCHS; do
|
||||||
# for attempt in 1 2 3; do
|
echo "Copying ${arch} image to DockerHub..."
|
||||||
# if docker buildx imagetools create \
|
for attempt in 1 2 3; do
|
||||||
# --tag "docker.io/homeassistant/${arch}-homeassistant:${VERSION}" \
|
if docker buildx imagetools create \
|
||||||
# "ghcr.io/home-assistant/${arch}-homeassistant:${VERSION}"; then
|
--tag "docker.io/homeassistant/${arch}-homeassistant:${VERSION}" \
|
||||||
# break
|
"ghcr.io/home-assistant/${arch}-homeassistant:${VERSION}"; then
|
||||||
# fi
|
break
|
||||||
# echo "Attempt ${attempt} failed, retrying in 10 seconds..."
|
fi
|
||||||
# sleep 10
|
echo "Attempt ${attempt} failed, retrying in 10 seconds..."
|
||||||
# if [ "${attempt}" -eq 3 ]; then
|
sleep 10
|
||||||
# echo "Failed after 3 attempts"
|
if [ "${attempt}" -eq 3 ]; then
|
||||||
# exit 1
|
echo "Failed after 3 attempts"
|
||||||
# fi
|
exit 1
|
||||||
# done
|
fi
|
||||||
# cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${VERSION}"
|
done
|
||||||
# done
|
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${VERSION}"
|
||||||
#
|
done
|
||||||
# - name: Create and push multi-arch manifests
|
|
||||||
# shell: bash
|
- name: Create and push multi-arch manifests
|
||||||
# env:
|
shell: bash
|
||||||
# ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
env:
|
||||||
# REGISTRY: ${{ matrix.registry }}
|
ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
||||||
# VERSION: ${{ needs.init.outputs.version }}
|
REGISTRY: ${{ matrix.registry }}
|
||||||
# META_TAGS: ${{ steps.meta.outputs.tags }}
|
VERSION: ${{ needs.init.outputs.version }}
|
||||||
# run: |
|
META_TAGS: ${{ steps.meta.outputs.tags }}
|
||||||
# # Build list of architecture images dynamically
|
run: |
|
||||||
# ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]')
|
# Build list of architecture images dynamically
|
||||||
# ARCH_IMAGES=()
|
ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]')
|
||||||
# for arch in $ARCHS; do
|
ARCH_IMAGES=()
|
||||||
# ARCH_IMAGES+=("${REGISTRY}/${arch}-homeassistant:${VERSION}")
|
for arch in $ARCHS; do
|
||||||
# done
|
ARCH_IMAGES+=("${REGISTRY}/${arch}-homeassistant:${VERSION}")
|
||||||
#
|
done
|
||||||
# # Build list of all tags for single manifest creation
|
|
||||||
# # Note: Using sep-tags=',' in metadata-action for easier parsing
|
# Build list of all tags for single manifest creation
|
||||||
# TAG_ARGS=()
|
# Note: Using sep-tags=',' in metadata-action for easier parsing
|
||||||
# IFS=',' read -ra TAGS <<< "${META_TAGS}"
|
TAG_ARGS=()
|
||||||
# for tag in "${TAGS[@]}"; do
|
IFS=',' read -ra TAGS <<< "${META_TAGS}"
|
||||||
# TAG_ARGS+=("--tag" "${tag}")
|
for tag in "${TAGS[@]}"; do
|
||||||
# done
|
TAG_ARGS+=("--tag" "${tag}")
|
||||||
#
|
done
|
||||||
# # Create manifest with ALL tags in a single operation (much faster!)
|
|
||||||
# echo "Creating multi-arch manifest with tags: ${TAGS[*]}"
|
# Create manifest with ALL tags in a single operation (much faster!)
|
||||||
# docker buildx imagetools create "${TAG_ARGS[@]}" "${ARCH_IMAGES[@]}"
|
echo "Creating multi-arch manifest with tags: ${TAGS[*]}"
|
||||||
#
|
docker buildx imagetools create "${TAG_ARGS[@]}" "${ARCH_IMAGES[@]}"
|
||||||
# # Sign each tag separately (signing requires individual tag names)
|
|
||||||
# echo "Signing all tags..."
|
# Sign each tag separately (signing requires individual tag names)
|
||||||
# for tag in "${TAGS[@]}"; do
|
echo "Signing all tags..."
|
||||||
# echo "Signing ${tag}"
|
for tag in "${TAGS[@]}"; do
|
||||||
# cosign sign --yes "${tag}"
|
echo "Signing ${tag}"
|
||||||
# done
|
cosign sign --yes "${tag}"
|
||||||
#
|
done
|
||||||
# echo "All manifests created and signed successfully"
|
|
||||||
#
|
echo "All manifests created and signed successfully"
|
||||||
# build_python:
|
|
||||||
# name: Build PyPi package
|
build_python:
|
||||||
# environment: ${{ needs.init.outputs.channel }}
|
name: Build PyPi package
|
||||||
# needs: ["init", "build_base"]
|
environment: ${{ needs.init.outputs.channel }}
|
||||||
# runs-on: ubuntu-latest
|
needs: ["init", "build_base"]
|
||||||
# permissions:
|
runs-on: ubuntu-latest
|
||||||
# contents: read # To check out the repository
|
permissions:
|
||||||
# id-token: write # For PyPI trusted publishing
|
contents: read # To check out the repository
|
||||||
# if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
id-token: write # For PyPI trusted publishing
|
||||||
# steps:
|
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||||
# - name: Checkout the repository
|
steps:
|
||||||
# uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- name: Checkout the repository
|
||||||
# with:
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
# persist-credentials: false
|
with:
|
||||||
#
|
persist-credentials: false
|
||||||
# - name: Set up Python
|
|
||||||
# uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
- name: Set up Python
|
||||||
# with:
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
# python-version-file: ".python-version"
|
with:
|
||||||
#
|
python-version-file: ".python-version"
|
||||||
# - name: Download translations
|
|
||||||
# uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
- name: Download translations
|
||||||
# with:
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
# name: translations
|
with:
|
||||||
#
|
name: translations
|
||||||
# - name: Extract translations
|
|
||||||
# run: |
|
- name: Extract translations
|
||||||
# tar xvf translations.tar.gz
|
run: |
|
||||||
# rm translations.tar.gz
|
tar xvf translations.tar.gz
|
||||||
#
|
rm translations.tar.gz
|
||||||
# - name: Build package
|
|
||||||
# shell: bash
|
- name: Build package
|
||||||
# run: |
|
shell: bash
|
||||||
# # Remove dist, build, and homeassistant.egg-info
|
run: |
|
||||||
# # when build locally for testing!
|
# Remove dist, build, and homeassistant.egg-info
|
||||||
# pip install build
|
# when build locally for testing!
|
||||||
# python -m build
|
pip install build
|
||||||
#
|
python -m build
|
||||||
# - name: Upload package to PyPI
|
|
||||||
# uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
- name: Upload package to PyPI
|
||||||
# with:
|
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||||
# skip-existing: true
|
with:
|
||||||
#
|
skip-existing: true
|
||||||
# hassfest-image:
|
|
||||||
# name: Build and test hassfest image
|
hassfest-image:
|
||||||
# runs-on: ubuntu-latest
|
name: Build and test hassfest image
|
||||||
# permissions:
|
runs-on: ubuntu-latest
|
||||||
# contents: read # To check out the repository
|
permissions:
|
||||||
# packages: write # To push to GHCR
|
contents: read # To check out the repository
|
||||||
# attestations: write # For build provenance attestation
|
packages: write # To push to GHCR
|
||||||
# id-token: write # For build provenance attestation
|
attestations: write # For build provenance attestation
|
||||||
# needs: ["init"]
|
id-token: write # For build provenance attestation
|
||||||
# if: github.repository_owner == 'home-assistant'
|
needs: ["init"]
|
||||||
# env:
|
if: github.repository_owner == 'home-assistant'
|
||||||
# HASSFEST_IMAGE_NAME: ghcr.io/home-assistant/hassfest
|
env:
|
||||||
# HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
HASSFEST_IMAGE_NAME: ghcr.io/home-assistant/hassfest
|
||||||
# steps:
|
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||||
# - name: Checkout repository
|
steps:
|
||||||
# uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- name: Checkout repository
|
||||||
# with:
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
# persist-credentials: false
|
with:
|
||||||
#
|
persist-credentials: false
|
||||||
# - name: Login to GitHub Container Registry
|
|
||||||
# uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
- name: Login to GitHub Container Registry
|
||||||
# with:
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
# registry: ghcr.io
|
with:
|
||||||
# username: ${{ github.repository_owner }}
|
registry: ghcr.io
|
||||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
username: ${{ github.repository_owner }}
|
||||||
#
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
# - name: Build Docker image
|
|
||||||
# uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
- name: Build Docker image
|
||||||
# with:
|
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||||
# context: . # So action will not pull the repository again
|
with:
|
||||||
# file: ./script/hassfest/docker/Dockerfile
|
context: . # So action will not pull the repository again
|
||||||
# load: true
|
file: ./script/hassfest/docker/Dockerfile
|
||||||
# tags: ${{ env.HASSFEST_IMAGE_TAG }}
|
load: true
|
||||||
#
|
tags: ${{ env.HASSFEST_IMAGE_TAG }}
|
||||||
# - name: Run hassfest against core
|
|
||||||
# run: docker run --rm -v "${GITHUB_WORKSPACE}":/github/workspace "${HASSFEST_IMAGE_TAG}" --core-path=/github/workspace
|
- name: Run hassfest against core
|
||||||
#
|
run: docker run --rm -v "${GITHUB_WORKSPACE}":/github/workspace "${HASSFEST_IMAGE_TAG}" --core-path=/github/workspace
|
||||||
# - name: Push Docker image
|
|
||||||
# if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
- name: Push Docker image
|
||||||
# id: push
|
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||||
# uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
id: push
|
||||||
# with:
|
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||||
# context: . # So action will not pull the repository again
|
with:
|
||||||
# file: ./script/hassfest/docker/Dockerfile
|
context: . # So action will not pull the repository again
|
||||||
# push: true
|
file: ./script/hassfest/docker/Dockerfile
|
||||||
# tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest
|
push: true
|
||||||
#
|
tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest
|
||||||
# - name: Generate artifact attestation
|
|
||||||
# if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
- name: Generate artifact attestation
|
||||||
# uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||||
# with:
|
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||||
# subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
with:
|
||||||
# subject-digest: ${{ steps.push.outputs.digest }}
|
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||||
# push-to-registry: true
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
push-to-registry: true
|
||||||
|
|||||||
+34
-34
@@ -40,7 +40,7 @@ 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"
|
||||||
ADDITIONAL_PYTHON_VERSIONS: "[]"
|
ADDITIONAL_PYTHON_VERSIONS: "[]"
|
||||||
# 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)
|
||||||
@@ -120,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
|
||||||
@@ -135,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
|
||||||
@@ -280,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
|
||||||
@@ -301,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
|
||||||
|
|
||||||
@@ -364,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: >-
|
||||||
@@ -372,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: >-
|
||||||
@@ -384,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: |
|
||||||
@@ -430,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 }}
|
||||||
@@ -484,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 }}
|
||||||
@@ -515,7 +515,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full 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
|
||||||
@@ -552,7 +552,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full 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
|
||||||
@@ -643,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
|
||||||
@@ -694,7 +694,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full 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
|
||||||
@@ -747,7 +747,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full 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
|
||||||
@@ -804,7 +804,7 @@ jobs:
|
|||||||
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 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
|
||||||
@@ -812,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: >-
|
||||||
@@ -854,7 +854,7 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
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 }}
|
||||||
@@ -887,7 +887,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full 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
|
||||||
@@ -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@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -1387,12 +1387,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.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
|
||||||
@@ -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
|
||||||
@@ -1558,12 +1558,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.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 }} # zizmor: ignore[secrets-outside-env]
|
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
|
||||||
@@ -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@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.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
|
||||||
|
|||||||
@@ -121,12 +121,12 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.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@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
@@ -172,17 +172,17 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.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@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.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@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: requirements_all_wheels
|
name: requirements_all_wheels
|
||||||
|
|
||||||
|
|||||||
@@ -137,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.*
|
||||||
@@ -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.*
|
||||||
@@ -574,6 +578,7 @@ 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.*
|
||||||
|
|||||||
Generated
+36
-14
@@ -214,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
|
||||||
@@ -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
|
||||||
@@ -733,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
|
||||||
@@ -780,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
|
||||||
@@ -939,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
|
||||||
@@ -1065,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
|
||||||
@@ -1212,12 +1226,12 @@ 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
|
/homeassistant/components/opendisplay/ @g4bri3lDev
|
||||||
/tests/components/opendisplay/ @g4bri3lDev
|
/tests/components/opendisplay/ @g4bri3lDev
|
||||||
/homeassistant/components/openerz/ @misialq
|
/homeassistant/components/openerz/ @misialq
|
||||||
@@ -1303,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
|
||||||
@@ -1561,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
|
||||||
@@ -1588,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
|
||||||
@@ -1616,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
|
||||||
@@ -1699,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
|
||||||
@@ -1744,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
|
||||||
@@ -1831,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
|
||||||
@@ -1915,6 +1935,8 @@ 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
|
/homeassistant/components/window/ @home-assistant/core
|
||||||
|
|||||||
Generated
+1
-1
@@ -29,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
|
||||||
|
|
||||||
|
|||||||
@@ -238,15 +238,23 @@ DEFAULT_INTEGRATIONS = {
|
|||||||
"timer",
|
"timer",
|
||||||
#
|
#
|
||||||
# Base platforms:
|
# Base platforms:
|
||||||
*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:
|
# Integrations providing triggers and conditions for base platforms:
|
||||||
|
"air_quality",
|
||||||
|
"battery",
|
||||||
"door",
|
"door",
|
||||||
"garage_door",
|
"garage_door",
|
||||||
"gate",
|
"gate",
|
||||||
"humidity",
|
"humidity",
|
||||||
|
"illuminance",
|
||||||
|
"moisture",
|
||||||
"motion",
|
"motion",
|
||||||
"occupancy",
|
"occupancy",
|
||||||
|
"power",
|
||||||
|
"temperature",
|
||||||
"window",
|
"window",
|
||||||
}
|
}
|
||||||
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
|
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
|
||||||
@@ -462,6 +470,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> bool:
|
|||||||
translation.async_setup(hass)
|
translation.async_setup(hass)
|
||||||
|
|
||||||
recovery = hass.config.recovery_mode
|
recovery = hass.config.recovery_mode
|
||||||
|
device_registry.async_setup(hass)
|
||||||
try:
|
try:
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
||||||
|
|||||||
@@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -87,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"])
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
"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."
|
"incomplete_discovery": "The discovered air-Q device did not provide a device ID. Ensure the firmware is up to date."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioamazondevices"],
|
"loggers": ["aioamazondevices"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aioamazondevices==13.0.1"]
|
"requirements": ["aioamazondevices==13.3.1"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.1"]
|
"requirements": ["pyanglianwater==3.1.2"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,8 +2,8 @@
|
|||||||
|
|
||||||
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
|
||||||
@@ -54,36 +54,31 @@ async def _run_client(
|
|||||||
client = runtime_data.client
|
client = runtime_data.client
|
||||||
coordinators = runtime_data.coordinators
|
coordinators = runtime_data.coordinators
|
||||||
|
|
||||||
def _listen(_: Any) -> None:
|
|
||||||
for coordinator in coordinators.values():
|
|
||||||
coordinator.async_notify_data_updated()
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for coordinator in coordinators.values():
|
|
||||||
await coordinator.state.start()
|
|
||||||
|
|
||||||
with client.listen(_listen):
|
|
||||||
for coordinator in coordinators.values():
|
for coordinator in coordinators.values():
|
||||||
coordinator.async_notify_connected()
|
await stack.enter_async_context(
|
||||||
await client.process()
|
coordinator.async_monitor_client()
|
||||||
finally:
|
)
|
||||||
await client.stop()
|
|
||||||
|
|
||||||
_LOGGER.debug("Client disconnected %s", client.host)
|
await client.process()
|
||||||
for coordinator in coordinators.values():
|
finally:
|
||||||
coordinator.async_notify_disconnected()
|
_LOGGER.debug("Client disconnected %s", 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)
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from arcam.fmj import ConnectionFailed
|
from arcam.fmj import ConnectionFailed
|
||||||
from arcam.fmj.client import Client
|
from arcam.fmj.client import AmxDuetResponse, Client, ResponsePacket
|
||||||
from arcam.fmj.state import State
|
from arcam.fmj.state import State
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@@ -51,7 +53,7 @@ class ArcamFmjCoordinator(DataUpdateCoordinator[None]):
|
|||||||
)
|
)
|
||||||
self.client = client
|
self.client = client
|
||||||
self.state = State(client, zone)
|
self.state = State(client, zone)
|
||||||
self.last_update_success = False
|
self.update_in_progress = False
|
||||||
|
|
||||||
name = config_entry.title
|
name = config_entry.title
|
||||||
unique_id = config_entry.unique_id or config_entry.entry_id
|
unique_id = config_entry.unique_id or config_entry.entry_id
|
||||||
@@ -74,24 +76,34 @@ class ArcamFmjCoordinator(DataUpdateCoordinator[None]):
|
|||||||
async def _async_update_data(self) -> None:
|
async def _async_update_data(self) -> None:
|
||||||
"""Fetch data for manual refresh."""
|
"""Fetch data for manual refresh."""
|
||||||
try:
|
try:
|
||||||
|
self.update_in_progress = True
|
||||||
await self.state.update()
|
await self.state.update()
|
||||||
except ConnectionFailed as err:
|
except ConnectionFailed as err:
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
f"Connection failed during update for zone {self.state.zn}"
|
f"Connection failed during update for zone {self.state.zn}"
|
||||||
) from err
|
) from err
|
||||||
|
finally:
|
||||||
|
self.update_in_progress = False
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_notify_data_updated(self) -> None:
|
def _async_notify_packet(self, packet: ResponsePacket | AmxDuetResponse) -> None:
|
||||||
"""Notify that new data has been received from the device."""
|
"""Packet callback to detect changes to state."""
|
||||||
self.async_set_updated_data(None)
|
if (
|
||||||
|
not isinstance(packet, ResponsePacket)
|
||||||
|
or packet.zn != self.state.zn
|
||||||
|
or self.update_in_progress
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_notify_connected(self) -> None:
|
|
||||||
"""Handle client connected."""
|
|
||||||
self.hass.async_create_task(self.async_refresh())
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_notify_disconnected(self) -> None:
|
|
||||||
"""Handle client disconnected."""
|
|
||||||
self.last_update_success = False
|
|
||||||
self.async_update_listeners()
|
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())
|
||||||
|
|||||||
@@ -26,3 +26,8 @@ class ArcamFmjEntity(CoordinatorEntity[ArcamFmjCoordinator]):
|
|||||||
if description is not None:
|
if description is not None:
|
||||||
self._attr_unique_id = f"{self._attr_unique_id}-{description.key}"
|
self._attr_unique_id = f"{self._attr_unique_id}-{description.key}"
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return if entity is available."""
|
||||||
|
return super().available and self.coordinator.client.connected
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -626,7 +626,7 @@ def websocket_delete_all_refresh_tokens(
|
|||||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle delete all refresh tokens request."""
|
"""Handle delete all refresh tokens request."""
|
||||||
current_refresh_token: RefreshToken
|
current_refresh_token: RefreshToken | None = None
|
||||||
remove_failed = False
|
remove_failed = False
|
||||||
token_type = msg.get("token_type")
|
token_type = msg.get("token_type")
|
||||||
delete_current_token = msg.get("delete_current_token")
|
delete_current_token = msg.get("delete_current_token")
|
||||||
@@ -654,7 +654,7 @@ def websocket_delete_all_refresh_tokens(
|
|||||||
else:
|
else:
|
||||||
connection.send_result(msg["id"], {})
|
connection.send_result(msg["id"], {})
|
||||||
|
|
||||||
async def _delete_current_token_soon() -> None:
|
async def _delete_current_token_soon(current_refresh_token: RefreshToken) -> None:
|
||||||
"""Delete the current token after a delay.
|
"""Delete the current token after a delay.
|
||||||
|
|
||||||
We do not want to delete the current token immediately as it will
|
We do not want to delete the current token immediately as it will
|
||||||
@@ -675,13 +675,15 @@ def websocket_delete_all_refresh_tokens(
|
|||||||
# the token right away.
|
# the token right away.
|
||||||
hass.auth.async_remove_refresh_token(current_refresh_token)
|
hass.auth.async_remove_refresh_token(current_refresh_token)
|
||||||
|
|
||||||
if delete_current_token and (
|
if (
|
||||||
not limit_token_types or current_refresh_token.token_type == token_type
|
delete_current_token
|
||||||
|
and current_refresh_token
|
||||||
|
and (not limit_token_types or current_refresh_token.token_type == token_type)
|
||||||
):
|
):
|
||||||
# Deleting the token will close the connection so we need
|
# Deleting the token will close the connection so we need
|
||||||
# to do it with a delay in a tracked task to ensure it still
|
# to do it with a delay in a tracked task to ensure it still
|
||||||
# happens if Home Assistant is shutting down.
|
# happens if Home Assistant is shutting down.
|
||||||
hass.async_create_task(_delete_current_token_soon())
|
hass.async_create_task(_delete_current_token_soon(current_refresh_token))
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ def async_setup(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Component to allow users to login."""
|
"""Component to allow users to login."""
|
||||||
hass.http.register_view(WellKnownOAuthInfoView)
|
hass.http.register_view(WellKnownOAuthInfoView)
|
||||||
|
hass.http.register_view(WellKnownProtectedResourceView)
|
||||||
hass.http.register_view(AuthProvidersView)
|
hass.http.register_view(AuthProvidersView)
|
||||||
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow, store_result))
|
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow, store_result))
|
||||||
hass.http.register_view(LoginFlowResourceView(hass.auth.login_flow, store_result))
|
hass.http.register_view(LoginFlowResourceView(hass.auth.login_flow, store_result))
|
||||||
@@ -141,6 +142,13 @@ class WellKnownOAuthInfoView(HomeAssistantView):
|
|||||||
"authorization_endpoint": f"{url_prefix}/auth/authorize",
|
"authorization_endpoint": f"{url_prefix}/auth/authorize",
|
||||||
"token_endpoint": f"{url_prefix}/auth/token",
|
"token_endpoint": f"{url_prefix}/auth/token",
|
||||||
"revocation_endpoint": f"{url_prefix}/auth/revoke",
|
"revocation_endpoint": f"{url_prefix}/auth/revoke",
|
||||||
|
# Home Assistant already accepts URL-based client_ids via
|
||||||
|
# IndieAuth without prior registration, which is compatible with
|
||||||
|
# draft-ietf-oauth-client-id-metadata-document. This flag
|
||||||
|
# advertises that support to encourage clients to use it. The
|
||||||
|
# metadata document is not actually fetched as IndieAuth doesn't
|
||||||
|
# require it.
|
||||||
|
"client_id_metadata_document_supported": True,
|
||||||
"response_types_supported": ["code"],
|
"response_types_supported": ["code"],
|
||||||
"service_documentation": (
|
"service_documentation": (
|
||||||
"https://developers.home-assistant.io/docs/auth_api"
|
"https://developers.home-assistant.io/docs/auth_api"
|
||||||
@@ -154,6 +162,32 @@ class WellKnownOAuthInfoView(HomeAssistantView):
|
|||||||
return self.json(metadata)
|
return self.json(metadata)
|
||||||
|
|
||||||
|
|
||||||
|
class WellKnownProtectedResourceView(HomeAssistantView):
|
||||||
|
"""View to host the OAuth2 Protected Resource Metadata per RFC9728."""
|
||||||
|
|
||||||
|
requires_auth = False
|
||||||
|
url = "/.well-known/oauth-protected-resource"
|
||||||
|
name = "well-known/oauth-protected-resource"
|
||||||
|
|
||||||
|
async def get(self, request: web.Request) -> web.Response:
|
||||||
|
"""Return the protected resource metadata."""
|
||||||
|
hass = request.app[KEY_HASS]
|
||||||
|
try:
|
||||||
|
url_prefix = get_url(hass, require_current_request=True)
|
||||||
|
except NoURLAvailableError:
|
||||||
|
return self.json_message("No URL available", HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
|
return self.json(
|
||||||
|
{
|
||||||
|
"resource": url_prefix,
|
||||||
|
"authorization_servers": [url_prefix],
|
||||||
|
"resource_documentation": (
|
||||||
|
"https://developers.home-assistant.io/docs/auth_api"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AuthProvidersView(HomeAssistantView):
|
class AuthProvidersView(HomeAssistantView):
|
||||||
"""View to get available auth providers."""
|
"""View to get available auth providers."""
|
||||||
|
|
||||||
|
|||||||
@@ -118,28 +118,13 @@ SERVICE_TRIGGER = "trigger"
|
|||||||
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
|
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
|
||||||
|
|
||||||
_EXPERIMENTAL_CONDITION_PLATFORMS = {
|
_EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||||
|
"air_quality",
|
||||||
"alarm_control_panel",
|
"alarm_control_panel",
|
||||||
"assist_satellite",
|
"assist_satellite",
|
||||||
|
"battery",
|
||||||
|
"calendar",
|
||||||
"climate",
|
"climate",
|
||||||
"cover",
|
"counter",
|
||||||
"device_tracker",
|
|
||||||
"fan",
|
|
||||||
"humidifier",
|
|
||||||
"lawn_mower",
|
|
||||||
"light",
|
|
||||||
"lock",
|
|
||||||
"media_player",
|
|
||||||
"person",
|
|
||||||
"siren",
|
|
||||||
"switch",
|
|
||||||
"vacuum",
|
|
||||||
}
|
|
||||||
|
|
||||||
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
|
||||||
"alarm_control_panel",
|
|
||||||
"assist_satellite",
|
|
||||||
"button",
|
|
||||||
"climate",
|
|
||||||
"cover",
|
"cover",
|
||||||
"device_tracker",
|
"device_tracker",
|
||||||
"door",
|
"door",
|
||||||
@@ -148,22 +133,69 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
|||||||
"gate",
|
"gate",
|
||||||
"humidifier",
|
"humidifier",
|
||||||
"humidity",
|
"humidity",
|
||||||
"input_boolean",
|
"illuminance",
|
||||||
"lawn_mower",
|
"lawn_mower",
|
||||||
"light",
|
"light",
|
||||||
"lock",
|
"lock",
|
||||||
"media_player",
|
"media_player",
|
||||||
|
"moisture",
|
||||||
"motion",
|
"motion",
|
||||||
"occupancy",
|
"occupancy",
|
||||||
"person",
|
"person",
|
||||||
|
"power",
|
||||||
|
"schedule",
|
||||||
|
"select",
|
||||||
|
"siren",
|
||||||
|
"switch",
|
||||||
|
"temperature",
|
||||||
|
"text",
|
||||||
|
"timer",
|
||||||
|
"vacuum",
|
||||||
|
"valve",
|
||||||
|
"water_heater",
|
||||||
|
"window",
|
||||||
|
}
|
||||||
|
|
||||||
|
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||||
|
"air_quality",
|
||||||
|
"alarm_control_panel",
|
||||||
|
"assist_satellite",
|
||||||
|
"battery",
|
||||||
|
"button",
|
||||||
|
"climate",
|
||||||
|
"counter",
|
||||||
|
"cover",
|
||||||
|
"device_tracker",
|
||||||
|
"door",
|
||||||
|
"event",
|
||||||
|
"fan",
|
||||||
|
"garage_door",
|
||||||
|
"gate",
|
||||||
|
"humidifier",
|
||||||
|
"humidity",
|
||||||
|
"illuminance",
|
||||||
|
"lawn_mower",
|
||||||
|
"light",
|
||||||
|
"lock",
|
||||||
|
"media_player",
|
||||||
|
"moisture",
|
||||||
|
"motion",
|
||||||
|
"occupancy",
|
||||||
|
"person",
|
||||||
|
"power",
|
||||||
"remote",
|
"remote",
|
||||||
"scene",
|
"scene",
|
||||||
"schedule",
|
"schedule",
|
||||||
|
"select",
|
||||||
"siren",
|
"siren",
|
||||||
"switch",
|
"switch",
|
||||||
|
"temperature",
|
||||||
"text",
|
"text",
|
||||||
|
"todo",
|
||||||
"update",
|
"update",
|
||||||
"vacuum",
|
"vacuum",
|
||||||
|
"valve",
|
||||||
|
"water_heater",
|
||||||
"window",
|
"window",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,11 +78,11 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"reload": {
|
"reload": {
|
||||||
"description": "Reloads the automation configuration.",
|
"description": "Reloads the automation configuration.",
|
||||||
"name": "[%key:common::action::reload%]"
|
"name": "Reload automations"
|
||||||
},
|
},
|
||||||
"toggle": {
|
"toggle": {
|
||||||
"description": "Toggles (enable / disable) an automation.",
|
"description": "Toggles (enable / disable) an automation.",
|
||||||
"name": "[%key:common::action::toggle%]"
|
"name": "Toggle automation"
|
||||||
},
|
},
|
||||||
"trigger": {
|
"trigger": {
|
||||||
"description": "Triggers the actions of an automation.",
|
"description": "Triggers the actions of an automation.",
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
"name": "Skip conditions"
|
"name": "Skip conditions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Trigger"
|
"name": "Trigger automation"
|
||||||
},
|
},
|
||||||
"turn_off": {
|
"turn_off": {
|
||||||
"description": "Disables an automation.",
|
"description": "Disables an automation.",
|
||||||
@@ -102,11 +102,11 @@
|
|||||||
"name": "Stop actions"
|
"name": "Stop actions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "[%key:common::action::turn_off%]"
|
"name": "Turn off automation"
|
||||||
},
|
},
|
||||||
"turn_on": {
|
"turn_on": {
|
||||||
"description": "Enables an automation.",
|
"description": "Enables an automation.",
|
||||||
"name": "[%key:common::action::turn_on%]"
|
"name": "Turn on automation"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Automation"
|
"title": "Automation"
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ class S3BackupAgent(BackupAgent):
|
|||||||
if backup.size < MULTIPART_MIN_PART_SIZE_BYTES:
|
if backup.size < MULTIPART_MIN_PART_SIZE_BYTES:
|
||||||
await self._upload_simple(tar_filename, open_stream)
|
await self._upload_simple(tar_filename, open_stream)
|
||||||
else:
|
else:
|
||||||
await self._upload_multipart(tar_filename, open_stream)
|
await self._upload_multipart(tar_filename, open_stream, on_progress)
|
||||||
|
|
||||||
# Upload the metadata file
|
# Upload the metadata file
|
||||||
metadata_content = json.dumps(backup.as_dict())
|
metadata_content = json.dumps(backup.as_dict())
|
||||||
@@ -188,11 +188,13 @@ class S3BackupAgent(BackupAgent):
|
|||||||
self,
|
self,
|
||||||
tar_filename: str,
|
tar_filename: str,
|
||||||
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
||||||
):
|
on_progress: OnProgressCallback,
|
||||||
|
) -> None:
|
||||||
"""Upload a large file using multipart upload.
|
"""Upload a large file using multipart upload.
|
||||||
|
|
||||||
:param tar_filename: The target filename for the backup.
|
:param tar_filename: The target filename for the backup.
|
||||||
:param open_stream: A function returning an async iterator that yields bytes.
|
:param open_stream: A function returning an async iterator that yields bytes.
|
||||||
|
:param on_progress: A callback to report the number of uploaded bytes.
|
||||||
"""
|
"""
|
||||||
_LOGGER.debug("Starting multipart upload for %s", tar_filename)
|
_LOGGER.debug("Starting multipart upload for %s", tar_filename)
|
||||||
multipart_upload = await self._client.create_multipart_upload(
|
multipart_upload = await self._client.create_multipart_upload(
|
||||||
@@ -205,6 +207,7 @@ class S3BackupAgent(BackupAgent):
|
|||||||
part_number = 1
|
part_number = 1
|
||||||
buffer = bytearray() # bytes buffer to store the data
|
buffer = bytearray() # bytes buffer to store the data
|
||||||
offset = 0 # start index of unread data inside buffer
|
offset = 0 # start index of unread data inside buffer
|
||||||
|
bytes_uploaded = 0
|
||||||
|
|
||||||
stream = await open_stream()
|
stream = await open_stream()
|
||||||
async for chunk in stream:
|
async for chunk in stream:
|
||||||
@@ -233,6 +236,8 @@ class S3BackupAgent(BackupAgent):
|
|||||||
Body=part_data.tobytes(),
|
Body=part_data.tobytes(),
|
||||||
)
|
)
|
||||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||||
|
bytes_uploaded += len(part_data)
|
||||||
|
on_progress(bytes_uploaded=bytes_uploaded)
|
||||||
part_number += 1
|
part_number += 1
|
||||||
finally:
|
finally:
|
||||||
view.release()
|
view.release()
|
||||||
@@ -261,6 +266,8 @@ class S3BackupAgent(BackupAgent):
|
|||||||
Body=remaining_data.tobytes(),
|
Body=remaining_data.tobytes(),
|
||||||
)
|
)
|
||||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||||
|
bytes_uploaded += len(remaining_data)
|
||||||
|
on_progress(bytes_uploaded=bytes_uploaded)
|
||||||
|
|
||||||
await cast(Any, self._client).complete_multipart_upload(
|
await cast(Any, self._client).complete_multipart_upload(
|
||||||
Bucket=self._bucket,
|
Bucket=self._bucket,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
|||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.httpx_client import get_async_client
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from ..const import LOGGER
|
from ..const import LOGGER
|
||||||
from ..errors import AuthenticationRequired, CannotConnect
|
from ..errors import AuthenticationRequired, CannotConnect
|
||||||
@@ -26,7 +26,7 @@ async def get_axis_api(
|
|||||||
config: Mapping[str, Any],
|
config: Mapping[str, Any],
|
||||||
) -> axis.AxisDevice:
|
) -> axis.AxisDevice:
|
||||||
"""Create a Axis device API."""
|
"""Create a Axis device API."""
|
||||||
session = get_async_client(hass, verify_ssl=False)
|
session = async_get_clientsession(hass, verify_ssl=False)
|
||||||
|
|
||||||
api = axis.AxisDevice(
|
api = axis.AxisDevice(
|
||||||
Configuration(
|
Configuration(
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["axis"],
|
"loggers": ["axis"],
|
||||||
"requirements": ["axis==66"],
|
"requirements": ["axis==67"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "AXIS"
|
"manufacturer": "AXIS"
|
||||||
|
|||||||
@@ -34,4 +34,4 @@ EXCLUDE_DATABASE_FROM_BACKUP = [
|
|||||||
"home-assistant_v2.db-wal",
|
"home-assistant_v2.db-wal",
|
||||||
]
|
]
|
||||||
|
|
||||||
SECURETAR_CREATE_VERSION = 2
|
SECURETAR_CREATE_VERSION = 3
|
||||||
|
|||||||
@@ -246,6 +246,8 @@ def decrypt_backup(
|
|||||||
except (DecryptError, SecureTarError, tarfile.TarError) as err:
|
except (DecryptError, SecureTarError, tarfile.TarError) as err:
|
||||||
LOGGER.warning("Error decrypting backup: %s", err)
|
LOGGER.warning("Error decrypting backup: %s", err)
|
||||||
error = err
|
error = err
|
||||||
|
except Abort:
|
||||||
|
raise
|
||||||
except Exception as err: # noqa: BLE001
|
except Exception as err: # noqa: BLE001
|
||||||
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
|
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
|
||||||
error = err
|
error = err
|
||||||
@@ -332,8 +334,10 @@ def encrypt_backup(
|
|||||||
except (EncryptError, SecureTarError, tarfile.TarError) as err:
|
except (EncryptError, SecureTarError, tarfile.TarError) as err:
|
||||||
LOGGER.warning("Error encrypting backup: %s", err)
|
LOGGER.warning("Error encrypting backup: %s", err)
|
||||||
error = err
|
error = err
|
||||||
|
except Abort:
|
||||||
|
raise
|
||||||
except Exception as err: # noqa: BLE001
|
except Exception as err: # noqa: BLE001
|
||||||
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
|
LOGGER.exception("Unexpected error when encrypting backup: %s", err)
|
||||||
error = err
|
error = err
|
||||||
else:
|
else:
|
||||||
# Pad the output stream to the requested minimum size
|
# Pad the output stream to the requested minimum size
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
"""Integration for battery triggers and conditions."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
DOMAIN = "battery"
|
||||||
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
__all__ = []
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up the component."""
|
||||||
|
return True
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""Provides conditions for batteries."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
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 PERCENTAGE, 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_state_condition,
|
||||||
|
)
|
||||||
|
|
||||||
|
BATTERY_DOMAIN_SPECS = {
|
||||||
|
BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.BATTERY)
|
||||||
|
}
|
||||||
|
BATTERY_CHARGING_DOMAIN_SPECS = {
|
||||||
|
BINARY_SENSOR_DOMAIN: DomainSpec(
|
||||||
|
device_class=BinarySensorDeviceClass.BATTERY_CHARGING
|
||||||
|
)
|
||||||
|
}
|
||||||
|
BATTERY_PERCENTAGE_DOMAIN_SPECS = {
|
||||||
|
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.BATTERY),
|
||||||
|
}
|
||||||
|
|
||||||
|
CONDITIONS: dict[str, type[Condition]] = {
|
||||||
|
"is_low": make_entity_state_condition(BATTERY_DOMAIN_SPECS, STATE_ON),
|
||||||
|
"is_not_low": make_entity_state_condition(BATTERY_DOMAIN_SPECS, STATE_OFF),
|
||||||
|
"is_charging": make_entity_state_condition(BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON),
|
||||||
|
"is_not_charging": make_entity_state_condition(
|
||||||
|
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF
|
||||||
|
),
|
||||||
|
"is_level": make_entity_numerical_condition(
|
||||||
|
BATTERY_PERCENTAGE_DOMAIN_SPECS, PERCENTAGE
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||||
|
"""Return the conditions for batteries."""
|
||||||
|
return CONDITIONS
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
.condition_common: &condition_common
|
||||||
|
target: &target_battery_binary_sensor
|
||||||
|
entity:
|
||||||
|
- domain: binary_sensor
|
||||||
|
device_class: battery
|
||||||
|
fields:
|
||||||
|
behavior: &condition_behavior
|
||||||
|
required: true
|
||||||
|
default: any
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
translation_key: condition_behavior
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- any
|
||||||
|
|
||||||
|
.battery_threshold_entity: &battery_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
- domain: sensor
|
||||||
|
device_class: battery
|
||||||
|
- domain: number
|
||||||
|
device_class: battery
|
||||||
|
|
||||||
|
.battery_threshold_number: &battery_threshold_number
|
||||||
|
min: 0
|
||||||
|
max: 100
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
|
||||||
|
is_low: *condition_common
|
||||||
|
|
||||||
|
is_not_low: *condition_common
|
||||||
|
|
||||||
|
is_charging:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
- domain: binary_sensor
|
||||||
|
device_class: battery_charging
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
|
||||||
|
is_not_charging:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
- domain: binary_sensor
|
||||||
|
device_class: battery_charging
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
|
||||||
|
is_level:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: battery
|
||||||
|
fields:
|
||||||
|
behavior: *condition_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *battery_threshold_entity
|
||||||
|
mode: is
|
||||||
|
number: *battery_threshold_number
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"is_charging": {
|
||||||
|
"condition": "mdi:battery-charging"
|
||||||
|
},
|
||||||
|
"is_level": {
|
||||||
|
"condition": "mdi:battery-unknown"
|
||||||
|
},
|
||||||
|
"is_low": {
|
||||||
|
"condition": "mdi:battery-alert"
|
||||||
|
},
|
||||||
|
"is_not_charging": {
|
||||||
|
"condition": "mdi:battery"
|
||||||
|
},
|
||||||
|
"is_not_low": {
|
||||||
|
"condition": "mdi:battery"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"triggers": {
|
||||||
|
"level_changed": {
|
||||||
|
"trigger": "mdi:battery-unknown"
|
||||||
|
},
|
||||||
|
"level_crossed_threshold": {
|
||||||
|
"trigger": "mdi:battery-alert"
|
||||||
|
},
|
||||||
|
"low": {
|
||||||
|
"trigger": "mdi:battery-alert"
|
||||||
|
},
|
||||||
|
"not_low": {
|
||||||
|
"trigger": "mdi:battery"
|
||||||
|
},
|
||||||
|
"started_charging": {
|
||||||
|
"trigger": "mdi:battery-charging"
|
||||||
|
},
|
||||||
|
"stopped_charging": {
|
||||||
|
"trigger": "mdi:battery"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"domain": "battery",
|
||||||
|
"name": "Battery",
|
||||||
|
"codeowners": ["@home-assistant/core"],
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/battery",
|
||||||
|
"integration_type": "system",
|
||||||
|
"quality_scale": "internal"
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"condition_behavior_name": "Condition passes if",
|
||||||
|
"condition_threshold_name": "Threshold type",
|
||||||
|
"trigger_behavior_name": "Trigger when",
|
||||||
|
"trigger_threshold_name": "Threshold type"
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"is_charging": {
|
||||||
|
"description": "Tests if one or more batteries are charging.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Battery is charging"
|
||||||
|
},
|
||||||
|
"is_level": {
|
||||||
|
"description": "Tests the battery level of one or more batteries.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::battery::common::condition_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Battery level"
|
||||||
|
},
|
||||||
|
"is_low": {
|
||||||
|
"description": "Tests if one or more batteries are low.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Battery is low"
|
||||||
|
},
|
||||||
|
"is_not_charging": {
|
||||||
|
"description": "Tests if one or more batteries are not charging.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Battery is not charging"
|
||||||
|
},
|
||||||
|
"is_not_low": {
|
||||||
|
"description": "Tests if one or more batteries are not low.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Battery is not low"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selector": {
|
||||||
|
"condition_behavior": {
|
||||||
|
"options": {
|
||||||
|
"all": "All",
|
||||||
|
"any": "Any"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trigger_behavior": {
|
||||||
|
"options": {
|
||||||
|
"any": "Any",
|
||||||
|
"first": "First",
|
||||||
|
"last": "Last"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Battery",
|
||||||
|
"triggers": {
|
||||||
|
"level_changed": {
|
||||||
|
"description": "Triggers after the battery level of one or more batteries changes.",
|
||||||
|
"fields": {
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::battery::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Battery level changed"
|
||||||
|
},
|
||||||
|
"level_crossed_threshold": {
|
||||||
|
"description": "Triggers after the battery level of one or more batteries crosses a threshold.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"name": "[%key:component::battery::common::trigger_threshold_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Battery level crossed threshold"
|
||||||
|
},
|
||||||
|
"low": {
|
||||||
|
"description": "Triggers after one or more batteries become low.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Battery low"
|
||||||
|
},
|
||||||
|
"not_low": {
|
||||||
|
"description": "Triggers after one or more batteries are no longer low.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Battery not low"
|
||||||
|
},
|
||||||
|
"started_charging": {
|
||||||
|
"description": "Triggers after one or more batteries start charging.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Battery started charging"
|
||||||
|
},
|
||||||
|
"stopped_charging": {
|
||||||
|
"description": "Triggers after one or more batteries stop charging.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::battery::common::trigger_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Battery stopped charging"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""Provides triggers for batteries."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
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 STATE_OFF, STATE_ON
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.automation import DomainSpec
|
||||||
|
from homeassistant.helpers.trigger import (
|
||||||
|
Trigger,
|
||||||
|
make_entity_numerical_state_changed_trigger,
|
||||||
|
make_entity_numerical_state_crossed_threshold_trigger,
|
||||||
|
make_entity_target_state_trigger,
|
||||||
|
)
|
||||||
|
|
||||||
|
BATTERY_LOW_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||||
|
BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.BATTERY),
|
||||||
|
}
|
||||||
|
|
||||||
|
BATTERY_CHARGING_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||||
|
BINARY_SENSOR_DOMAIN: DomainSpec(
|
||||||
|
device_class=BinarySensorDeviceClass.BATTERY_CHARGING
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
BATTERY_PERCENTAGE_DOMAIN_SPECS: dict[str, DomainSpec] = {
|
||||||
|
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.BATTERY),
|
||||||
|
}
|
||||||
|
|
||||||
|
TRIGGERS: dict[str, type[Trigger]] = {
|
||||||
|
"low": make_entity_target_state_trigger(BATTERY_LOW_DOMAIN_SPECS, STATE_ON),
|
||||||
|
"not_low": make_entity_target_state_trigger(BATTERY_LOW_DOMAIN_SPECS, STATE_OFF),
|
||||||
|
"started_charging": make_entity_target_state_trigger(
|
||||||
|
BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON
|
||||||
|
),
|
||||||
|
"stopped_charging": make_entity_target_state_trigger(
|
||||||
|
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF
|
||||||
|
),
|
||||||
|
"level_changed": make_entity_numerical_state_changed_trigger(
|
||||||
|
BATTERY_PERCENTAGE_DOMAIN_SPECS, valid_unit="%"
|
||||||
|
),
|
||||||
|
"level_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||||
|
BATTERY_PERCENTAGE_DOMAIN_SPECS, valid_unit="%"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||||
|
"""Return the triggers for batteries."""
|
||||||
|
return TRIGGERS
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
.trigger_common_fields:
|
||||||
|
behavior: &trigger_behavior
|
||||||
|
required: true
|
||||||
|
default: any
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
translation_key: trigger_behavior
|
||||||
|
options:
|
||||||
|
- first
|
||||||
|
- last
|
||||||
|
- any
|
||||||
|
|
||||||
|
.battery_threshold_entity: &battery_threshold_entity
|
||||||
|
- domain: input_number
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
- domain: number
|
||||||
|
device_class: battery
|
||||||
|
- domain: sensor
|
||||||
|
device_class: battery
|
||||||
|
|
||||||
|
.battery_threshold_number: &battery_threshold_number
|
||||||
|
min: 0
|
||||||
|
max: 100
|
||||||
|
mode: box
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
|
||||||
|
.trigger_target_battery: &trigger_target_battery
|
||||||
|
entity:
|
||||||
|
- domain: binary_sensor
|
||||||
|
device_class: battery
|
||||||
|
|
||||||
|
.trigger_target_charging: &trigger_target_charging
|
||||||
|
entity:
|
||||||
|
- domain: binary_sensor
|
||||||
|
device_class: battery_charging
|
||||||
|
|
||||||
|
.trigger_target_percentage: &trigger_target_percentage
|
||||||
|
entity:
|
||||||
|
- domain: sensor
|
||||||
|
device_class: battery
|
||||||
|
|
||||||
|
low:
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
target: *trigger_target_battery
|
||||||
|
|
||||||
|
not_low:
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
target: *trigger_target_battery
|
||||||
|
|
||||||
|
started_charging:
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
target: *trigger_target_charging
|
||||||
|
|
||||||
|
stopped_charging:
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
target: *trigger_target_charging
|
||||||
|
|
||||||
|
level_changed:
|
||||||
|
target: *trigger_target_percentage
|
||||||
|
fields:
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *battery_threshold_entity
|
||||||
|
mode: changed
|
||||||
|
number: *battery_threshold_number
|
||||||
|
|
||||||
|
level_crossed_threshold:
|
||||||
|
target: *trigger_target_percentage
|
||||||
|
fields:
|
||||||
|
behavior: *trigger_behavior
|
||||||
|
threshold:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
numeric_threshold:
|
||||||
|
entity: *battery_threshold_entity
|
||||||
|
mode: crossed
|
||||||
|
number: *battery_threshold_number
|
||||||
@@ -1 +1 @@
|
|||||||
"""The bbox component."""
|
"""The Bbox integration."""
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
"""The bitcoin component."""
|
"""The Bitcoin integration."""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "blebox",
|
"domain": "blebox",
|
||||||
"name": "BleBox devices",
|
"name": "BleBox devices",
|
||||||
"codeowners": ["@bbx-a", "@swistakm"],
|
"codeowners": ["@bbx-a", "@swistakm", "@bkobus-bbx"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/blebox",
|
"documentation": "https://www.home-assistant.io/integrations/blebox",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
"""The blinksticklight component."""
|
"""The BlinkStick integration."""
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Support for Blinkstick lights."""
|
"""Support for BlinkStick lights."""
|
||||||
|
|
||||||
# mypy: ignore-errors
|
# mypy: ignore-errors
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -40,7 +40,7 @@ def setup_platform(
|
|||||||
add_entities: AddEntitiesCallback,
|
add_entities: AddEntitiesCallback,
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Blinkstick device specified by serial number."""
|
"""Set up BlinkStick device specified by serial number."""
|
||||||
|
|
||||||
name = config[CONF_NAME]
|
name = config[CONF_NAME]
|
||||||
serial = config[CONF_SERIAL]
|
serial = config[CONF_SERIAL]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/bluesound",
|
"documentation": "https://www.home-assistant.io/integrations/bluesound",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["pyblu==2.0.5"],
|
"requirements": ["pyblu==2.0.6"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"type": "_musc._tcp.local."
|
"type": "_musc._tcp.local."
|
||||||
|
|||||||
@@ -21,6 +21,6 @@
|
|||||||
"bluetooth-auto-recovery==1.5.3",
|
"bluetooth-auto-recovery==1.5.3",
|
||||||
"bluetooth-data-tools==1.28.4",
|
"bluetooth-data-tools==1.28.4",
|
||||||
"dbus-fast==3.1.2",
|
"dbus-fast==3.1.2",
|
||||||
"habluetooth==5.10.2"
|
"habluetooth==5.11.1"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""The BMW Connected Drive integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import issue_registry as ir
|
||||||
|
|
||||||
|
DOMAIN = "bmw_connected_drive"
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up BMW Connected Drive from a config entry."""
|
||||||
|
ir.async_create_issue(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
DOMAIN,
|
||||||
|
is_fixable=False,
|
||||||
|
severity=ir.IssueSeverity.ERROR,
|
||||||
|
translation_key="integration_removed",
|
||||||
|
translation_placeholders={
|
||||||
|
"entries": "/config/integrations/integration/bmw_connected_drive",
|
||||||
|
"custom_component_url": "https://github.com/kvanbiesen/bmw-cardata-ha",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Remove a config entry."""
|
||||||
|
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||||
|
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||||
|
# Remove any remaining disabled or ignored entries
|
||||||
|
for _entry in hass.config_entries.async_entries(DOMAIN):
|
||||||
|
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
"""The BMW Connected Drive integration config flow."""
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow
|
||||||
|
|
||||||
|
from . import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
class BMWConnectedDriveConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for BMW Connected Drive."""
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"domain": "bmw_connected_drive",
|
||||||
|
"name": "BMW Connected Drive",
|
||||||
|
"codeowners": [],
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||||
|
"integration_type": "system",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
"quality_scale": "legacy",
|
||||||
|
"requirements": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"issues": {
|
||||||
|
"integration_removed": {
|
||||||
|
"description": "The BMW Connected Drive integration has been removed from Home Assistant.\n\nIn September 2025, BMW blocked third-party access to their servers by adding additional security measures. For EU-registered cars, a community-developed [custom component]({custom_component_url}) using BMW's CarData API is available as an alternative.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing BMW Connected Drive integration entries]({entries}).",
|
||||||
|
"title": "The BMW Connected Drive integration has been removed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,7 +52,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
"""Rotate the access token."""
|
"""Rotate the access token."""
|
||||||
access_tokens.append(hex(_RND.getrandbits(256))[2:])
|
access_tokens.append(hex(_RND.getrandbits(256))[2:])
|
||||||
|
|
||||||
async_track_time_interval(hass, _rotate_token, TOKEN_CHANGE_INTERVAL)
|
async_track_time_interval(
|
||||||
|
hass, _rotate_token, TOKEN_CHANGE_INTERVAL, cancel_on_shutdown=True
|
||||||
|
)
|
||||||
|
|
||||||
hass.http.register_view(BrandsIntegrationView(hass))
|
hass.http.register_view(BrandsIntegrationView(hass))
|
||||||
hass.http.register_view(BrandsHardwareView(hass))
|
hass.http.register_view(BrandsHardwareView(hass))
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from homeassistant.helpers import config_validation as cv
|
|||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import CONF_PASSKEY, DOMAIN
|
from .const import CONF_PASSKEY, DOMAIN, LOGGER
|
||||||
from .coordinator import BSBLanFastCoordinator, BSBLanSlowCoordinator
|
from .coordinator import BSBLanFastCoordinator, BSBLanSlowCoordinator
|
||||||
from .services import async_setup_services
|
from .services import async_setup_services
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class BSBLanData:
|
|||||||
client: BSBLAN
|
client: BSBLAN
|
||||||
device: Device
|
device: Device
|
||||||
info: Info
|
info: Info
|
||||||
static: StaticState
|
static: StaticState | None
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
@@ -82,11 +82,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
|
|||||||
# the connection by fetching firmware version
|
# the connection by fetching firmware version
|
||||||
await bsblan.initialize()
|
await bsblan.initialize()
|
||||||
|
|
||||||
# Fetch device metadata in parallel for faster startup
|
# Fetch required device metadata in parallel for faster startup
|
||||||
device, info, static = await asyncio.gather(
|
device, info = await asyncio.gather(
|
||||||
bsblan.device(),
|
bsblan.device(),
|
||||||
bsblan.info(),
|
bsblan.info(),
|
||||||
bsblan.static_values(),
|
|
||||||
)
|
)
|
||||||
except BSBLANConnectionError as err:
|
except BSBLANConnectionError as err:
|
||||||
raise ConfigEntryNotReady(
|
raise ConfigEntryNotReady(
|
||||||
@@ -111,6 +110,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
|
|||||||
translation_key="setup_general_error",
|
translation_key="setup_general_error",
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
|
try:
|
||||||
|
static = await bsblan.static_values()
|
||||||
|
except (BSBLANError, TimeoutError) as err:
|
||||||
|
LOGGER.debug(
|
||||||
|
"Static values not available for %s: %s",
|
||||||
|
entry.data[CONF_HOST],
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
static = None
|
||||||
|
|
||||||
# Create coordinators with the already-initialized client
|
# Create coordinators with the already-initialized client
|
||||||
fast_coordinator = BSBLanFastCoordinator(hass, entry, bsblan)
|
fast_coordinator = BSBLanFastCoordinator(hass, entry, bsblan)
|
||||||
slow_coordinator = BSBLanSlowCoordinator(hass, entry, bsblan)
|
slow_coordinator = BSBLanSlowCoordinator(hass, entry, bsblan)
|
||||||
|
|||||||
@@ -90,10 +90,11 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
|
|||||||
self._attr_unique_id = f"{format_mac(data.device.MAC)}-climate"
|
self._attr_unique_id = f"{format_mac(data.device.MAC)}-climate"
|
||||||
|
|
||||||
# Set temperature range if available, otherwise use Home Assistant defaults
|
# Set temperature range if available, otherwise use Home Assistant defaults
|
||||||
if data.static.min_temp is not None and data.static.min_temp.value is not None:
|
if (static := data.static) is not None:
|
||||||
self._attr_min_temp = data.static.min_temp.value
|
if (min_temp := static.min_temp) is not None and min_temp.value is not None:
|
||||||
if data.static.max_temp is not None and data.static.max_temp.value is not None:
|
self._attr_min_temp = min_temp.value
|
||||||
self._attr_max_temp = data.static.max_temp.value
|
if (max_temp := static.max_temp) is not None and max_temp.value is not None:
|
||||||
|
self._attr_max_temp = max_temp.value
|
||||||
self._attr_temperature_unit = data.fast_coordinator.client.get_temperature_unit
|
self._attr_temperature_unit = data.fast_coordinator.client.get_temperature_unit
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -183,90 +183,122 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
existing_entry = self._get_reauth_entry()
|
existing_entry = self._get_reauth_entry()
|
||||||
|
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
# Preserve existing values as defaults
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="reauth_confirm",
|
step_id="reauth_confirm",
|
||||||
data_schema=vol.Schema(
|
data_schema=self._build_credentials_schema(existing_entry.data),
|
||||||
{
|
|
||||||
vol.Optional(
|
|
||||||
CONF_PASSKEY,
|
|
||||||
default=existing_entry.data.get(
|
|
||||||
CONF_PASSKEY, vol.UNDEFINED
|
|
||||||
),
|
|
||||||
): str,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_USERNAME,
|
|
||||||
default=existing_entry.data.get(
|
|
||||||
CONF_USERNAME, vol.UNDEFINED
|
|
||||||
),
|
|
||||||
): str,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_PASSWORD,
|
|
||||||
default=vol.UNDEFINED,
|
|
||||||
): str,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Combine existing data with the user's new input for validation.
|
# Merge existing data with user input for validation
|
||||||
# This correctly handles adding, changing, and clearing credentials.
|
validate_data = {**existing_entry.data, **user_input}
|
||||||
config_data = existing_entry.data.copy()
|
errors = await self._async_validate_credentials(validate_data)
|
||||||
config_data.update(user_input)
|
|
||||||
|
|
||||||
self.host = config_data[CONF_HOST]
|
if errors:
|
||||||
self.port = config_data[CONF_PORT]
|
return self.async_show_form(
|
||||||
self.passkey = config_data.get(CONF_PASSKEY)
|
step_id="reauth_confirm",
|
||||||
self.username = config_data.get(CONF_USERNAME)
|
data_schema=self._build_credentials_schema(user_input),
|
||||||
self.password = config_data.get(CONF_PASSWORD)
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
existing_entry, data_updates=user_input, reason="reauth_successful"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_reconfigure(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle reconfiguration flow."""
|
||||||
|
existing_entry = self._get_reconfigure_entry()
|
||||||
|
|
||||||
|
if user_input is None:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="reconfigure",
|
||||||
|
data_schema=self._build_connection_schema(existing_entry.data),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Merge existing data with user input for validation
|
||||||
|
validate_data = {**existing_entry.data, **user_input}
|
||||||
|
errors = await self._async_validate_credentials(validate_data)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="reconfigure",
|
||||||
|
data_schema=self._build_connection_schema(user_input),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prevent reconfiguring to a different physical device
|
||||||
|
# it gets the unique ID from the device info when it validates credentials
|
||||||
|
self._abort_if_unique_id_mismatch()
|
||||||
|
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
existing_entry,
|
||||||
|
data_updates=user_input,
|
||||||
|
reason="reconfigure_successful",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_validate_credentials(self, data: dict[str, Any]) -> dict[str, str]:
|
||||||
|
"""Validate connection credentials and return errors dict."""
|
||||||
|
self.host = data[CONF_HOST]
|
||||||
|
self.port = data.get(CONF_PORT, DEFAULT_PORT)
|
||||||
|
self.passkey = data.get(CONF_PASSKEY)
|
||||||
|
self.username = data.get(CONF_USERNAME)
|
||||||
|
self.password = data.get(CONF_PASSWORD)
|
||||||
|
|
||||||
|
errors: dict[str, str] = {}
|
||||||
try:
|
try:
|
||||||
await self._get_bsblan_info(raise_on_progress=False, is_reauth=True)
|
await self._get_bsblan_info(raise_on_progress=False, is_reauth=True)
|
||||||
except BSBLANAuthError:
|
except BSBLANAuthError:
|
||||||
return self.async_show_form(
|
errors["base"] = "invalid_auth"
|
||||||
step_id="reauth_confirm",
|
|
||||||
data_schema=vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Optional(
|
|
||||||
CONF_PASSKEY,
|
|
||||||
default=user_input.get(CONF_PASSKEY, vol.UNDEFINED),
|
|
||||||
): str,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_USERNAME,
|
|
||||||
default=user_input.get(CONF_USERNAME, vol.UNDEFINED),
|
|
||||||
): str,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_PASSWORD,
|
|
||||||
default=vol.UNDEFINED,
|
|
||||||
): str,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
errors={"base": "invalid_auth"},
|
|
||||||
)
|
|
||||||
except BSBLANError:
|
except BSBLANError:
|
||||||
return self.async_show_form(
|
errors["base"] = "cannot_connect"
|
||||||
step_id="reauth_confirm",
|
return errors
|
||||||
data_schema=vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Optional(
|
|
||||||
CONF_PASSKEY,
|
|
||||||
default=user_input.get(CONF_PASSKEY, vol.UNDEFINED),
|
|
||||||
): str,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_USERNAME,
|
|
||||||
default=user_input.get(CONF_USERNAME, vol.UNDEFINED),
|
|
||||||
): str,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_PASSWORD,
|
|
||||||
default=vol.UNDEFINED,
|
|
||||||
): str,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
errors={"base": "cannot_connect"},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update only the fields that were provided by the user
|
@callback
|
||||||
return self.async_update_reload_and_abort(
|
def _build_credentials_schema(self, defaults: Mapping[str, Any]) -> vol.Schema:
|
||||||
existing_entry, data_updates=user_input, reason="reauth_successful"
|
"""Build schema for credentials-only forms (reauth)."""
|
||||||
|
return vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PASSKEY,
|
||||||
|
default=defaults.get(CONF_PASSKEY) or vol.UNDEFINED,
|
||||||
|
): str,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_USERNAME,
|
||||||
|
default=defaults.get(CONF_USERNAME) or vol.UNDEFINED,
|
||||||
|
): str,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PASSWORD,
|
||||||
|
default=vol.UNDEFINED,
|
||||||
|
): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _build_connection_schema(self, defaults: Mapping[str, Any]) -> vol.Schema:
|
||||||
|
"""Build schema for full connection forms (user and reconfigure)."""
|
||||||
|
return vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(
|
||||||
|
CONF_HOST,
|
||||||
|
default=defaults.get(CONF_HOST, vol.UNDEFINED),
|
||||||
|
): str,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PORT,
|
||||||
|
default=defaults.get(CONF_PORT, DEFAULT_PORT),
|
||||||
|
): int,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PASSKEY,
|
||||||
|
default=defaults.get(CONF_PASSKEY) or vol.UNDEFINED,
|
||||||
|
): str,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_USERNAME,
|
||||||
|
default=defaults.get(CONF_USERNAME) or vol.UNDEFINED,
|
||||||
|
): str,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PASSWORD,
|
||||||
|
default=vol.UNDEFINED,
|
||||||
|
): str,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@@ -274,32 +306,9 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
self, errors: dict | None = None, user_input: dict[str, Any] | None = None
|
self, errors: dict | None = None, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Show the setup form to the user."""
|
"""Show the setup form to the user."""
|
||||||
# Preserve user input if provided, otherwise use defaults
|
|
||||||
defaults = user_input or {}
|
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
data_schema=vol.Schema(
|
data_schema=self._build_connection_schema(user_input or {}),
|
||||||
{
|
|
||||||
vol.Required(
|
|
||||||
CONF_HOST, default=defaults.get(CONF_HOST, vol.UNDEFINED)
|
|
||||||
): str,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_PORT, default=defaults.get(CONF_PORT, DEFAULT_PORT)
|
|
||||||
): int,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_PASSKEY, default=defaults.get(CONF_PASSKEY, vol.UNDEFINED)
|
|
||||||
): str,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_USERNAME,
|
|
||||||
default=defaults.get(CONF_USERNAME, vol.UNDEFINED),
|
|
||||||
): str,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_PASSWORD,
|
|
||||||
default=defaults.get(CONF_PASSWORD, vol.UNDEFINED),
|
|
||||||
): str,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
errors=errors or {},
|
errors=errors or {},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ async def async_get_config_entry_diagnostics(
|
|||||||
"sensor": data.fast_coordinator.data.sensor.model_dump(),
|
"sensor": data.fast_coordinator.data.sensor.model_dump(),
|
||||||
"dhw": data.fast_coordinator.data.dhw.model_dump(),
|
"dhw": data.fast_coordinator.data.dhw.model_dump(),
|
||||||
},
|
},
|
||||||
"static": data.static.model_dump(),
|
"static": data.static.model_dump() if data.static is not None else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add DHW config and schedule from slow coordinator if available
|
# Add DHW config and schedule from slow coordinator if available
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["bsblan"],
|
"loggers": ["bsblan"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["python-bsblan==5.1.2"],
|
"requirements": ["python-bsblan==5.1.3"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"name": "bsb-lan*",
|
"name": "bsb-lan*",
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ rules:
|
|||||||
entity-translations: done
|
entity-translations: done
|
||||||
exception-translations: done
|
exception-translations: done
|
||||||
icon-translations: todo
|
icon-translations: todo
|
||||||
reconfiguration-flow: todo
|
reconfiguration-flow: done
|
||||||
repair-issues:
|
repair-issues:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: |
|
comment: |
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
|
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||||
|
"unique_id_mismatch": "The device you are trying to reconfigure is not the same as the one previously configured."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
@@ -39,6 +41,24 @@
|
|||||||
"description": "The BSB-LAN integration needs to re-authenticate with {name}",
|
"description": "The BSB-LAN integration needs to re-authenticate with {name}",
|
||||||
"title": "[%key:common::config_flow::title::reauth%]"
|
"title": "[%key:common::config_flow::title::reauth%]"
|
||||||
},
|
},
|
||||||
|
"reconfigure": {
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
|
"passkey": "[%key:component::bsblan::config::step::user::data::passkey%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"port": "[%key:common::config_flow::data::port%]",
|
||||||
|
"username": "[%key:common::config_flow::data::username%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"host": "[%key:component::bsblan::config::step::user::data_description::host%]",
|
||||||
|
"passkey": "[%key:component::bsblan::config::step::user::data_description::passkey%]",
|
||||||
|
"password": "[%key:component::bsblan::config::step::user::data_description::password%]",
|
||||||
|
"port": "[%key:component::bsblan::config::step::user::data_description::port%]",
|
||||||
|
"username": "[%key:component::bsblan::config::step::user::data_description::username%]"
|
||||||
|
},
|
||||||
|
"description": "Update connection settings for your BSB-LAN device.",
|
||||||
|
"title": "Reconfigure BSB-LAN"
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
|
|||||||
@@ -23,8 +23,8 @@
|
|||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"press": {
|
"press": {
|
||||||
"description": "Presses a button entity.",
|
"description": "Presses a button.",
|
||||||
"name": "Press"
|
"name": "Press button"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Button",
|
"title": "Button",
|
||||||
|
|||||||
@@ -578,13 +578,13 @@ class CalendarEntity(Entity):
|
|||||||
return STATE_OFF
|
return STATE_OFF
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_write_ha_state(self) -> None:
|
def _async_write_ha_state(self) -> None:
|
||||||
"""Write the state to the state machine.
|
"""Write the state to the state machine.
|
||||||
|
|
||||||
This sets up listeners to handle state transitions for start or end of
|
This sets up listeners to handle state transitions for start or end of
|
||||||
the current or upcoming event.
|
the current or upcoming event.
|
||||||
"""
|
"""
|
||||||
super().async_write_ha_state()
|
super()._async_write_ha_state()
|
||||||
if self._alarm_unsubs is None:
|
if self._alarm_unsubs is None:
|
||||||
self._alarm_unsubs = []
|
self._alarm_unsubs = []
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""Provides conditions for calendars."""
|
||||||
|
|
||||||
|
from homeassistant.const import STATE_ON
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.condition import Condition, make_entity_state_condition
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
CONDITIONS: dict[str, type[Condition]] = {
|
||||||
|
"is_event_active": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||||
|
"""Return the calendar conditions."""
|
||||||
|
return CONDITIONS
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
is_event_active:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
- domain: calendar
|
||||||
|
fields:
|
||||||
|
behavior:
|
||||||
|
required: true
|
||||||
|
default: any
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
translation_key: condition_behavior
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- any
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
{
|
{
|
||||||
|
"conditions": {
|
||||||
|
"is_event_active": {
|
||||||
|
"condition": "mdi:calendar-check"
|
||||||
|
}
|
||||||
|
},
|
||||||
"entity_component": {
|
"entity_component": {
|
||||||
"_": {
|
"_": {
|
||||||
"default": "mdi:calendar",
|
"default": "mdi:calendar",
|
||||||
|
|||||||
@@ -1,4 +1,18 @@
|
|||||||
{
|
{
|
||||||
|
"common": {
|
||||||
|
"condition_behavior_name": "Condition passes if"
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"is_event_active": {
|
||||||
|
"description": "Tests if one or more calendars have an active event.",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"name": "[%key:component::calendar::common::condition_behavior_name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Calendar event is active"
|
||||||
|
}
|
||||||
|
},
|
||||||
"entity_component": {
|
"entity_component": {
|
||||||
"_": {
|
"_": {
|
||||||
"name": "[%key:component::calendar::title%]",
|
"name": "[%key:component::calendar::title%]",
|
||||||
@@ -46,6 +60,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"selector": {
|
"selector": {
|
||||||
|
"condition_behavior": {
|
||||||
|
"options": {
|
||||||
|
"all": "All",
|
||||||
|
"any": "Any"
|
||||||
|
}
|
||||||
|
},
|
||||||
"trigger_offset_type": {
|
"trigger_offset_type": {
|
||||||
"options": {
|
"options": {
|
||||||
"after": "After",
|
"after": "After",
|
||||||
@@ -90,7 +110,7 @@
|
|||||||
"name": "Summary"
|
"name": "Summary"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Create event"
|
"name": "Create calendar event"
|
||||||
},
|
},
|
||||||
"get_events": {
|
"get_events": {
|
||||||
"description": "Retrieves events on a calendar within a time range.",
|
"description": "Retrieves events on a calendar within a time range.",
|
||||||
@@ -108,7 +128,7 @@
|
|||||||
"name": "Start time"
|
"name": "Start time"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Get events"
|
"name": "Get calendar events"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Calendar",
|
"title": "Calendar",
|
||||||
|
|||||||
@@ -432,6 +432,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Entity Properties
|
# Entity Properties
|
||||||
|
entity_description: CameraEntityDescription
|
||||||
_attr_brand: str | None = None
|
_attr_brand: str | None = None
|
||||||
_attr_frame_interval: float = MIN_STREAM_INTERVAL
|
_attr_frame_interval: float = MIN_STREAM_INTERVAL
|
||||||
_attr_is_on: bool = True
|
_attr_is_on: bool = True
|
||||||
@@ -759,12 +760,12 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
return CameraCapabilities(frontend_stream_types)
|
return CameraCapabilities(frontend_stream_types)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_write_ha_state(self) -> None:
|
def _async_write_ha_state(self) -> None:
|
||||||
"""Write the state to the state machine.
|
"""Write the state to the state machine.
|
||||||
|
|
||||||
Schedules async_refresh_providers if support of streams have changed.
|
Schedules async_refresh_providers if support of streams have changed.
|
||||||
"""
|
"""
|
||||||
super().async_write_ha_state()
|
super()._async_write_ha_state()
|
||||||
if self.__supports_stream != (
|
if self.__supports_stream != (
|
||||||
supports_stream := self.supported_features & CameraEntityFeature.STREAM
|
supports_stream := self.supported_features & CameraEntityFeature.STREAM
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -51,11 +51,11 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"disable_motion_detection": {
|
"disable_motion_detection": {
|
||||||
"description": "Disables the motion detection of a camera.",
|
"description": "Disables the motion detection of a camera.",
|
||||||
"name": "Disable motion detection"
|
"name": "Disable camera motion detection"
|
||||||
},
|
},
|
||||||
"enable_motion_detection": {
|
"enable_motion_detection": {
|
||||||
"description": "Enables the motion detection of a camera.",
|
"description": "Enables the motion detection of a camera.",
|
||||||
"name": "Enable motion detection"
|
"name": "Enable camera motion detection"
|
||||||
},
|
},
|
||||||
"play_stream": {
|
"play_stream": {
|
||||||
"description": "Plays a camera stream on a supported media player.",
|
"description": "Plays a camera stream on a supported media player.",
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
"name": "Media player"
|
"name": "Media player"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Play stream"
|
"name": "Play camera stream"
|
||||||
},
|
},
|
||||||
"record": {
|
"record": {
|
||||||
"description": "Creates a recording of a live camera feed.",
|
"description": "Creates a recording of a live camera feed.",
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
"name": "Lookback"
|
"name": "Lookback"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Record"
|
"name": "Record camera feed"
|
||||||
},
|
},
|
||||||
"snapshot": {
|
"snapshot": {
|
||||||
"description": "Takes a snapshot from a camera.",
|
"description": "Takes a snapshot from a camera.",
|
||||||
@@ -97,15 +97,15 @@
|
|||||||
"name": "Filename"
|
"name": "Filename"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Take snapshot"
|
"name": "Take camera snapshot"
|
||||||
},
|
},
|
||||||
"turn_off": {
|
"turn_off": {
|
||||||
"description": "Turns off a camera.",
|
"description": "Turns off a camera.",
|
||||||
"name": "[%key:common::action::turn_off%]"
|
"name": "Turn off camera"
|
||||||
},
|
},
|
||||||
"turn_on": {
|
"turn_on": {
|
||||||
"description": "Turns on a camera.",
|
"description": "Turns on a camera.",
|
||||||
"name": "[%key:common::action::turn_on%]"
|
"name": "Turn on camera"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Camera"
|
"title": "Camera"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user