Compare commits

...

318 Commits

Author SHA1 Message Date
Martin Hjelmare 6ff21b70f2 Disable instead of fixing in lg_thinq 2026-06-08 13:56:14 +02:00
Martin Hjelmare d01903cd59 Fix lg_thinq 2026-06-08 13:10:54 +02:00
Martin Hjelmare 0d17f3062c Add tests for dt_util.UTC 2026-06-08 11:59:51 +02:00
Martin Hjelmare c706c83337 Update readme 2026-06-08 11:49:54 +02:00
Martin Hjelmare 20f30f76d1 Deduplicate checker id 2026-06-08 11:45:50 +02:00
Martin Hjelmare 966b89cc14 Add pylint enforce dt.now 2026-06-08 11:27:59 +02:00
Colin 707742f720 Bump python-openevse-http to 1.0.1 (#172982) 2026-06-08 10:46:27 +02:00
Ronald van der Meer f58e0e5234 Fix Duco box device removal on partial node refreshes (#173186) 2026-06-08 09:48:27 +02:00
robotsnh c3d6ad029f refactor(energyid): replace datetime.now with dt_util.utcnow (#173241) 2026-06-08 09:11:48 +02:00
Mark Purcell 630f442042 Bump pydaikin to 2.18.1 (#173249) 2026-06-08 09:05:28 +02:00
Manu 62419789b9 Add version to Uptime Kuma diagnostics (#173254) 2026-06-08 08:46:55 +02:00
Joakim Plate f2f5a55165 Store product type in gardena_bluetooth config entry (#173223) 2026-06-08 08:20:47 +02:00
Mick Vleeshouwer c6a57bc81a Bump pyOverkiz to 2.0.0 in Overkiz (#173212) 2026-06-07 22:09:52 -04:00
Raphael Hehl 4171f566e9 Bump uiprotect to 11.8.0 (#173227) 2026-06-07 22:08:49 -04:00
renovate[bot] 0ac9834d93 Update syrupy to 5.3.1 (#173245)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-07 22:07:48 -04:00
Paul Bottein d7673a08c8 Bump yoto-api to 4.0.2 (#173238) 2026-06-07 22:07:36 -04:00
J. Nick Koston 35cb7c6147 Bump aiohttp to 3.14.1 (#173242) 2026-06-07 22:07:05 -04:00
Paulus Schoutsen d098622021 Return all matches for duplicate names in GetLiveContext (#173157)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-07 21:50:15 -04:00
karwosts f88e757e51 Add a battery charging sensor to demo device (#173219) 2026-06-07 23:00:22 +02:00
Pierre Pinon 653e6a43fa fix(indevolt): unable to discharge at 0 (#173085) 2026-06-07 21:55:10 +02:00
Bram Kragten 1462e7a181 Update frontend to 20260527.5 (#173236) 2026-06-07 21:39:34 +02:00
Martin Claesson e34d821f7d Add Kiosker Clear Blackout Button (#173225) 2026-06-07 21:29:51 +02:00
G Johansson 02b4442a6c Fix config flow version in goodwe (#173235) 2026-06-07 21:26:40 +02:00
J. Nick Koston 809571443c Bump habluetooth to 6.8.3 (#173194) 2026-06-07 19:17:52 +02:00
mvn23 d59398e0ea Remove name fields from opentherm_gw config flow (#173159) 2026-06-07 18:21:21 +02:00
Raphael Hehl 9c9695d0ba Bump uiprotect to 11.3.0 (#173024)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-06-07 14:19:26 +02:00
Allen Porter 3fbdbb12e2 Support streaming updates for V1 Roborock devices (#173182) 2026-06-07 14:12:20 +02:00
Ronald van der Meer a29f2907f7 Use NodeType enum in Duco entity (#173189) 2026-06-07 14:07:48 +02:00
mvn23 83534f286e Ensure opentherm_gw boiler and thermostat manufacturers are strings (#173162) 2026-06-07 12:23:09 +02:00
Ronald van der Meer 4fe93f9c64 Fix uncaught Duco diagnostics client errors (#173191) 2026-06-07 07:27:02 +02:00
Shay Levy fd8789d599 Fix Shelly virtual component unit retrieval (#173183) 2026-06-07 00:34:18 +03:00
Joost Lekkerkerker d0b34dfe92 Have Plugwise handle unavailable temperature measurements (#173173) 2026-06-07 00:19:29 +03:00
Tomer 390766ba3a Bump victron-mqtt to 2026.6.1.1 (#173142) 2026-06-07 00:15:39 +03:00
Vincent Knoop Pathuis 3a46d1088b Refactor Landis+Gyr heat meter to use the HA standard SerialPortSelector (#173170) 2026-06-06 15:16:41 -04:00
epenet 26d56b8218 Use DOMAIN constant in test (async_setup_component o-z) (#173018) 2026-06-06 12:14:46 -07:00
Vincent Knoop Pathuis 6ee819cdc3 Bump to Ultraheat 0.6.1 (#173175) 2026-06-06 15:14:01 -04:00
Stefan Agner 1cf8fe4d0b Drop legacy requires_api_password from discovery announcement (#173090)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:54:36 -04:00
Michael Hansen c5f93cdd72 Validate sentences and answers (#173127)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2026-06-06 13:48:07 -04:00
Michael Hansen 42136f1464 Bubble up conversation response in script run (#173131)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-06 13:47:49 -04:00
J. Nick Koston 34f3452280 Wait for Shelly bluetooth proxy connection at startup (#173165) 2026-06-06 11:17:49 -05:00
Michael Hansen ba9248cc94 LLM: format numeric states with display precision (#173128) 2026-06-06 12:15:48 -04:00
Michael Hansen 018cd1333e Bump ollama library (#173129) 2026-06-06 12:14:59 -04:00
J. Nick Koston c72d723e0d Wait for ESPHome bluetooth proxy connection at startup (#173164) 2026-06-06 11:13:03 -05:00
Paul Bottein b9b36d9e12 Add card group browsing to the Yoto media browser (#173152) 2026-06-06 12:12:58 -04:00
Klaas Schoute b6f38c3cbb Update forecast_solar integration to v5.0.1 (#173151) 2026-06-06 15:26:14 +02:00
Bouwe Westerdijk a0162d2ff0 Bump plugwise to v1.11.4 (#173147) 2026-06-06 12:03:32 +02:00
robotsnh b6f018873b refactor(dwd_weather_warnings): change datetime.now to dt_util.utcnow (#173149) 2026-06-06 11:58:06 +02:00
Crocmagnon 43e21322ea Bump data-grand-lyon-ha to 0.8.0 (#173108) 2026-06-06 11:08:43 +02:00
tronikos 86ccc59a5f Bump opower to 0.18.3 (#173141) 2026-06-06 08:53:52 +02:00
Luke Lashley 2fce2547c7 Close the connection for disabled Roborock devices (#172277) 2026-06-06 08:19:23 +02:00
Luke Lashley 6b40278d08 Allow using a custom server for Roborock setup. (#171645)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-05 21:36:39 -07:00
jasonjhofmann 05bb8b94fa Add network MAC connection to AirVisual Pro devices (#173071)
Co-authored-by: jasonjhofmann <16144532+jasonjhofmann@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 06:29:32 +02:00
Joakim Plate 5ac3a8cdde Switch to active scanner for gardena (#173062) 2026-06-06 04:08:10 +02:00
Paulus Schoutsen 266fccf0cf Use SerialPortSelector for DSMR serial port configuration (#171103)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 21:35:07 -04:00
Joakim Plate a1e6a6f9a2 Fix process advertisement for active scans (#173116) 2026-06-05 19:42:45 +02:00
renovate[bot] 2fe406c6ff Update uv to 0.11.17 (#173060) 2026-06-05 19:34:33 +02:00
Paul Bottein e1249fef8f Bump yoto-api to 3.2.0 (#173119) 2026-06-05 19:33:13 +02:00
Michael Hansen 6f61e97f8e Bump voip-utils to 0.4.0 (#173118) 2026-06-05 19:21:49 +02:00
Noah Husby b65751e8ac Bump aiostreammagic to 2.13.2 (#173114) 2026-06-05 19:11:55 +02:00
dependabot[bot] ef4bf77b24 Bump github/gh-aw-actions from 0.77.0 to 0.77.3 (#173073)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-05 19:10:35 +02:00
Markus Tuominen 977a9ecdd2 Add entity-unique-id pylint quality scale checker (#172815)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
2026-06-05 17:35:48 +02:00
Erik Montnemery 9e79eba970 Give any connected scanner highest priority when deriving person state (#173107) 2026-06-05 17:30:39 +02:00
Martin Hjelmare 40073e598c Fix pylint utcnow checker for dt_util.UTC (#173083) 2026-06-05 16:14:57 +02:00
Erik Montnemery 627d5cc110 Do not use home zone coordinates for person when detected home by scanner (#173042) 2026-06-05 16:05:58 +02:00
Paul Bottein b1dbeca9ed Bump yoto-api to 3.1.6 (#173104) 2026-06-05 15:58:11 +02:00
Robert Resch 059bc8d676 Unify query token auth in http views (#173082) 2026-06-05 15:57:16 +02:00
Erik Montnemery 085f794407 Add test showing zone.async_active_zone prefers zone closest to center (#173099) 2026-06-05 15:27:44 +02:00
Jan Bouwhuis 3996db289d Clean up unused MQTT constants (#173095) 2026-06-05 14:33:14 +02:00
Ronald van der Meer 291585e48e Fix Duco mode end time sensor name (#173045) 2026-06-05 14:23:13 +02:00
Erwin Douna d9a125ce9b Portainer extend timeout for disk space coordinator (#173032) 2026-06-05 14:21:09 +02:00
Erik Montnemery 786c957909 Teach legacy zone condition and trigger about in_zones state attribute (#173074) 2026-06-05 14:04:23 +02:00
Markus Tuominen dd6830f1c5 Add sub-devices for Reolink dual lens cameras with per-lens sensors (#173037) 2026-06-05 14:04:10 +02:00
epenet 4dbe58afc6 Use explicit DOMAIN import in mqtt tests (#173093) 2026-06-05 14:01:28 +02:00
Nikolai Rahimi 6c72d4337d Fix Mitsubishi Comfort devices skipped due to unresolved local address (#172959) 2026-06-05 13:53:13 +02:00
epenet fcff5229d9 Fix incorrect constant usage in mqtt config flow (#172557) 2026-06-05 13:53:10 +02:00
Joost Lekkerkerker 8edd813d4b Bump pySmartThings to 4.0.1 (#173092) 2026-06-05 13:38:32 +02:00
Robert Resch 509866c0eb Bump wheels to 2026.06.0 (#173089) 2026-06-05 13:28:31 +02:00
EnjoyingM 9db5860d6b Wolflink Fix state_class for long term statistics (#173048) 2026-06-05 12:52:06 +02:00
Erwin Douna 6917223cb3 Tado refactor to utilize get_zone_states (#173075) 2026-06-05 12:39:43 +02:00
Jan Bouwhuis cc4637a703 Create certificate files before trying to migrate the MQTT config entry (#173087)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-05 12:12:34 +02:00
A. Gideonse 2b0d14d71e Bump api-indevolt to 1.8.5 (#173078) 2026-06-05 10:57:08 +02:00
renovate[bot] d0d85d8844 Update ruff (#173059)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-05 09:48:36 +02:00
BrettLynch123 eea3d9d4c4 Bump tesla-powerwall to 0.5.3 (#173058) 2026-06-05 09:29:13 +02:00
Erik Montnemery 48a690b267 Derive zone entity state from person in_zones state attribute (#172942) 2026-06-05 08:12:22 +02:00
Michael 07dc2346de Bump py-synologydsm-api to 2.9.0 (#173041) 2026-06-04 22:26:18 +02:00
Erik Montnemery 711830b01f Add tracking_type capability attribute to device tracker (#173027) 2026-06-04 21:19:19 +02:00
Erik Montnemery f9fea56a8c Add tests of legacy device tracker states to person tests (#173023) 2026-06-04 21:12:24 +02:00
Franck Nijhof 8aac0c5b6e Convert LinkPlay configuration_url to string for device registry (#173034) 2026-06-04 20:17:50 +02:00
jdoughty04 227c43630a Add media player missing image coverage (#172641) 2026-06-04 20:09:16 +02:00
fdebrus e2f3a3232e Vistapool: add diagnostics support (#172824)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 20:05:37 +02:00
fdebrus 3173e56bf0 Fix Vistapool button test isolation by deepcopying _LED_DATA. (#172829)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 19:54:44 +02:00
bkobus-bbx e22b03f942 Add support for openSensor and drutexSmart (#169910) 2026-06-04 19:46:31 +02:00
fdebrus 467c2fdd57 Add light platform to Vistapool (#172549)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-04 19:43:56 +02:00
J. Diego Rodríguez Royo d825b6afa8 Sort Home Connect service.yaml programs (#172848) 2026-06-04 19:43:24 +02:00
J. Diego Rodríguez Royo 69fb1e142c Fix platfoms fixtures return type at Home Connect (#172849) 2026-06-04 19:43:04 +02:00
J. Diego Rodríguez Royo 80e71660e6 Avoid re-registering listeners at common.py from Home Connect (#172851) 2026-06-04 19:42:40 +02:00
Erwin Douna 045ba4e1dd API refactor to replace assert (#172862)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-04 19:31:46 +02:00
David Bonnes 983501406f Deprecate Evohome's refresh_system action (#169894)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-04 19:25:22 +02:00
Franck Nijhof 837308ba39 Merge branch 'master' into dev 2026-06-04 17:12:57 +00:00
Michael Hansen 6e53787d98 Bump hassil to 3.6.0 (#173031) 2026-06-04 18:51:02 +02:00
G Johansson 7dbce7863a Bump holidays to 0.98 (#173029) 2026-06-04 18:24:53 +02:00
Lukas e1d90fd244 Add source selection to samsung_infrared media player (#172794) 2026-06-04 18:19:59 +02:00
fdebrus bbeb2ac667 Vistapool: Add reconfiguration flow (#172836)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-04 17:55:08 +02:00
Jan Bouwhuis 21260bf1ab Fix value template in MQTT Fan and Siren subentry setup (#172980) 2026-06-04 17:53:06 +02:00
Marcello a0d67b80ab Fix offline devices in Fluss (#172833) 2026-06-04 17:39:34 +02:00
bkobus-bbx a6b7641d47 Add diagnostics for Blebox integration (#172556)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-04 17:35:27 +02:00
bkobus-bbx ad2db2ae88 Add exception translations for Blebox integration (#172560) 2026-06-04 17:22:33 +02:00
Raphael Hehl fc2b7902a5 Bump av to 17.0.1 (#172892) 2026-06-04 17:02:29 +02:00
bkobus-bbx 3aa4cbeeb0 Add icon translations for Blebox integration (#172565) 2026-06-04 16:58:51 +02:00
Yardian Support 3c2f171158 Bump pyyardian to 1.4.0 (#173020)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-04 16:54:51 +02:00
Erik Montnemery ffc6eeadc2 Always include template errors in trace (#172917) 2026-06-04 16:28:05 +02:00
mbo18 5cc1a0a1ef Add Motionblinds virtual integration Avosdim (#172821) 2026-06-04 16:22:06 +02:00
Erik Montnemery 1cbbce5b35 Fix person in_zones propagation from scanner in home zone (#173007) 2026-06-04 15:50:06 +02:00
Anatosun 9f5cb635f0 Upgrade Swisscom integration (#171816)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-04 14:54:54 +02:00
epenet 50de2c070e Use DOMAIN constant in test (hass.states.async_entity_ids) (#173009) 2026-06-04 14:01:51 +02:00
Samuel Xiao a7f012350f Switchbot Cloud: Fixed an issue where condition filtering for enabled Webhooks was abnormal (#172903) 2026-06-04 13:46:57 +02:00
Yardian Support 04d2211d1e Refactor Yardian zones into sub-devices using via_device (#172835) 2026-06-04 13:36:38 +02:00
Markus Adrario 5b4c2c6017 Homee: Add stop_tilt action for covers (#172952) 2026-06-04 13:19:09 +02:00
Joost Lekkerkerker f36a491ebd Fix double annotations for Pylint (#172477) 2026-06-04 13:18:33 +02:00
Ermanno Baschiera bde3b6e59f Add filter reset button to Helty Flow (#172866)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 13:15:31 +02:00
kohai-ut 53211bc37a Add tests for the envisalink integration (#172621)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 13:13:29 +02:00
Eric Stern 863655dce5 Fix SleepIQ 401 storm by isolating client session cookies (#172276) 2026-06-04 13:12:34 +02:00
epenet eb2a3d0ffd Prefer DOMAIN constant over config_flow.DOMAIN in tests (#172992) 2026-06-04 12:43:08 +02:00
epenet cbc7a5ae8e Use DOMAIN constant in tests (hass.services.async_call) (#172998) 2026-06-04 12:36:10 +02:00
epenet 90d45432dc Use DOMAIN constant in tests (async_mock_service) (#173002) 2026-06-04 12:35:54 +02:00
epenet 9e1abe6dfd Remove duplicate description in Tuya binary sensor (#173006) 2026-06-04 12:35:09 +02:00
epenet 682f0ba217 Use DOMAIN constant in test (config_entries.flow.async_init) (#173008) 2026-06-04 12:33:44 +02:00
Tomasz Dylewski 2b499d5aca Updated pajgps-api to version 0.4.0 (#172986) 2026-06-04 12:27:01 +02:00
Maciej Bieniek 11535f27da Bump imgw_pib to 2.2.2 (#172999) 2026-06-04 12:26:23 +02:00
Erik Montnemery f5fa2e244d Improve person tests (#172997) 2026-06-04 11:46:06 +02:00
epenet 1a63a38234 Add hass.states.async_entity_ids to domain constant checker (#172923) 2026-06-04 10:33:16 +02:00
J. Nick Koston ccb0b6a286 Bump aiodiscover to 3.3.1 (#172882) 2026-06-04 10:28:34 +02:00
bk86a 5e8f0d1078 Fix Lyric sensor crash when next_period_time is None (#167831)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 10:19:49 +02:00
epenet af6b0879de Use DOMAIN constant in MockConfigEntry (p-z) (#172991) 2026-06-04 10:19:05 +02:00
epenet 6450e2790c Use DOMAIN constant in MockConfigEntry (a-o) (#172989) 2026-06-04 10:18:51 +02:00
Abílio Costa 8aafebc5a6 Bump idasen-ha to 2.7.0 (#172962) 2026-06-04 10:15:17 +02:00
Ronald van der Meer e9c3a65e58 Bump python-duco-connectivity to 0.6.0 (#172938) 2026-06-04 09:45:56 +02:00
Jan Čermák a30ab26dda Bump aiohasupervisor to 0.5.0 (#172933) 2026-06-04 09:38:58 +02:00
dependabot[bot] aa3ae4986b Bump actions/ai-inference from 2.1.0 to 2.1.1 (#172966)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 09:10:08 +02:00
dependabot[bot] 3235e6d458 Bump github/gh-aw-actions from 0.76.0 to 0.77.0 (#172979)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 09:05:09 +02:00
Erik Montnemery d8975b3d0d Improve test of zone entity state (#172941)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-04 09:03:57 +02:00
BrettLynch123 4261787be1 Add operation mode sensor to Powerwall (#172967) 2026-06-04 08:42:24 +02:00
renovate[bot] 2e8097cc1f Update infrared-protocols to 6.0.0 (#172968)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-04 05:01:55 +02:00
Abílio Costa 9430bb2e32 Add Edifier Infrared integration (#172342)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-03 22:50:35 +01:00
J. Nick Koston 1283420fc6 Bump onvif-zeep-async to 4.2.0 (#172957) 2026-06-04 00:49:14 +03:00
starkillerOG 88e85e4325 Add more Reolink diagnostic info (#172945) 2026-06-03 23:34:18 +02:00
Erwin Douna 7cb08fdabc Incomfort refactor coordinator (#160953)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-03 23:31:18 +02:00
Tom 0cde867f93 Bump airOS to add insecure ssl detection (#172947) 2026-06-03 22:24:09 +02:00
J. Diego Rodríguez Royo 149daf4f97 Bump aiohomeconnect to 0.36.1 (#172946) 2026-06-03 22:13:25 +02:00
Thomas55555 6ffc32159b Bump aioautomower to 2.7.6 (#172937) 2026-06-03 22:00:04 +02:00
Ronald van der Meer 1c2d1013e6 Refactor Duco config flow tests to use small helpers (#172498) 2026-06-03 21:57:12 +02:00
Markus Adrario af4eaed5ed Homee: Use constants for cover states for readability (#172840) 2026-06-03 21:34:56 +02:00
Erik Montnemery dba09c334a Use zone DOMAIN constant in zone conditions (#172940) 2026-06-03 20:44:35 +02:00
Erik Montnemery c329bb4000 Don't log configuration errors when executing WS subscribe_trigger (#172918) 2026-06-03 19:58:32 +02:00
Josef Zweck 2e041dd45f Add reason for unvailability to opendisplay (#172909) 2026-06-03 19:48:56 +02:00
Franck Nijhof 836740c247 2026.6.0 (#172932) 2026-06-03 19:43:19 +02:00
rjones-gentex 8ba3e6c8c1 Upgrade HomeLink package, set integration type (#172371) 2026-06-03 19:43:17 +02:00
Chris 8db064c929 Add binary sensor platform to openevse (#172924) 2026-06-03 19:37:55 +02:00
Kurt Chrisford 8124544125 Bump actron-neo-api to 0.5.12 (#172902) 2026-06-03 19:15:09 +02:00
Mark 6291179292 Add Rabbit Air fan preset icons (#172931) 2026-06-03 19:00:45 +02:00
epenet fcaa11d09a Fix CI failure due to missing ssdp patching in braviatv (#172561) 2026-06-03 16:56:29 +00:00
Franck Nijhof bd985a2db2 Bump version to 2026.6.0 2026-06-03 16:31:34 +00:00
Joost Lekkerkerker 89a033bc2c Remove state attributes from OPNsense (#172930) 2026-06-03 16:30:52 +00:00
Joost Lekkerkerker 6f880ac8a9 Remove state attributes from OPNsense (#172930) 2026-06-03 18:29:14 +02:00
Paulus Schoutsen 7babb2423b Migrate itach to pyitachip2ir2==0.0.8 (#172908)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-03 18:16:42 +02:00
starkillerOG e812cd3c3f Bump reolink_aio to 0.20.1 (#172927) 2026-06-03 16:15:53 +00:00
Rasmus Graham 7e1874ae96 Bump vsure to 2.7.0 (#172856) 2026-06-03 17:08:34 +01:00
starkillerOG 1f50582a16 Bump reolink_aio to 0.20.1 (#172927) 2026-06-03 18:04:56 +02:00
Markus Tuominen 53211759cb Document missing pylint rules in plugin README (#172925) 2026-06-03 18:54:20 +03:00
Franck Nijhof ad99929178 Bump version to 2026.6.0b4 2026-06-03 15:09:31 +00:00
Bram Kragten d2672050cf Update frontend to 20260527.4 (#172907) 2026-06-03 15:08:41 +00:00
Sören 74fd636aa6 Add Avea Bluetooth reachability diagnostics (#172898) 2026-06-03 15:08:39 +00:00
Erik Montnemery b4f8fce912 Don't log condition errors when executing WS test_condition (#172897) 2026-06-03 15:08:37 +00:00
Michael Hansen 78a97f99dc Bump intents to 2026.6.1 (#172842) 2026-06-03 15:07:07 +00:00
Abílio Costa 4593059db2 Add "review" claude skill and use it in "gitbhub-pr-review" (#172797)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-03 15:57:21 +01:00
epenet 593ae9eb80 Add pylint plugin for correct use of DOMAIN constants in tests (#172693)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Markus Tuominen <3738613+Markus98@users.noreply.github.com>
2026-06-03 16:53:15 +02:00
Erik Montnemery 37b4bcaa39 Don't log condition errors when executing WS test_condition (#172897) 2026-06-03 16:11:54 +02:00
Bram Kragten 6bda3ea3a5 Update frontend to 20260527.4 (#172907) 2026-06-03 14:17:30 +02:00
Sören f4db5fb346 Add Avea Bluetooth reachability diagnostics (#172898) 2026-06-03 13:43:33 +02:00
Franck Nijhof 5d0565f007 Bump version to 2026.6.0b3 2026-06-03 10:03:15 +00:00
Erik Montnemery 083af9ccc7 Add zone occupancy conditions (#172896) 2026-06-03 10:02:17 +00:00
Erik Montnemery 6c87284dee Catch errors when setting up condition in WS subscribe_condition (#172895) 2026-06-03 10:02:15 +00:00
Paulus Schoutsen 0e0b29d16e Regenerate mdi_icons.py for frontend 20260527.3 (#172887) 2026-06-03 10:02:13 +00:00
Bram Kragten 8e493d84f1 Bump frontend to 20260527.3 (#172873) 2026-06-03 10:00:42 +00:00
Joost Lekkerkerker 4e2bc610e3 Bump pySmartThings to 4.0.0 (#172858) 2026-06-03 09:59:38 +00:00
jameson_uk 82d83feda4 Bump aioamazondevices to 14.0.0 (#172857) 2026-06-03 09:59:36 +00:00
Petro31 265fe6d338 Add translations for template device trackers in_zones option (#172850) 2026-06-03 09:59:34 +00:00
Wendelin bb8036f2c8 Automation choose: Add optional note to options (#172837) 2026-06-03 09:59:32 +00:00
Erik Montnemery 387b84ec7b Prevent log spam when WS subscribe_condition is active (#172832) 2026-06-03 09:59:30 +00:00
zhangluofeng 24037fcfa3 Don't create switch entity for switch device type in XThings Cloud (#172828) 2026-06-03 09:59:28 +00:00
Erik Montnemery 994b210588 Make the renamed trigger behavior options backwards compatible (#172822) 2026-06-03 09:59:26 +00:00
Franck Nijhof db6f1426ec Fix SwitchBot Blind Tilt KeyError on idle BLE advertisements (#172816) 2026-06-03 09:59:24 +00:00
Erik Montnemery 8ce5ba2ba4 Add zone conditions in / not in zone (#172810) 2026-06-03 09:59:22 +00:00
Matthias Alphart b176fb2113 Update knx-frontend to 2026.6.1.213802 (#172806) 2026-06-03 09:59:20 +00:00
Pete Sage ada8a98f87 Log warning on unsupported announce media formats for Sonos (#172614)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-03 09:59:18 +00:00
Heikki Henriksen 763d9879bf prusalink: guard non-string original in config_flow workaround (#172375) 2026-06-03 09:59:16 +00:00
Pete Sage 7bbd0ea472 Replace usages of datetime.now(UTC) with dt_util for Sonos (#172737) 2026-06-03 09:53:03 +00:00
Heikki Henriksen f04b0ee2c6 prusalink: guard non-string original in config_flow workaround (#172375) 2026-06-03 11:47:19 +02:00
jameson_uk 60f458a372 alexa devices - media player code quality (#172650) 2026-06-03 09:43:11 +00:00
Erik Montnemery 05eada2569 Add zone triggers occupancy detected/cleared (#172438) 2026-06-03 09:35:43 +00:00
Erik Montnemery d2abd7f6ca Add zone entered left triggers (#172412) 2026-06-03 09:35:41 +00:00
Erik Montnemery 52c3e17de9 Add zone occupancy conditions (#172896) 2026-06-03 11:20:13 +02:00
Imou-OpenPlatform 96c286f2e0 Add Imou integration (#161412)
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-03 11:18:31 +02:00
renovate[bot] 3e356de4e1 Update pytest-asyncio to 1.4.0 (#172886) 2026-06-03 11:17:32 +02:00
Erik Montnemery 90a874d81b Use zone DOMAIN constant in zone triggers (#172894) 2026-06-03 11:15:50 +02:00
Erik Montnemery 165024c6c9 Catch errors when setting up condition in WS subscribe_condition (#172895) 2026-06-03 11:06:18 +02:00
Erik Montnemery 66e4db3c0e Add zone conditions in / not in zone (#172810) 2026-06-03 10:40:43 +02:00
Franck Nijhof 0e6128c657 Fix SwitchBot Blind Tilt KeyError on idle BLE advertisements (#172816) 2026-06-03 10:37:16 +02:00
Wendelin 16febb36ba Automation choose: Add optional note to options (#172837) 2026-06-03 10:12:52 +02:00
Erik Montnemery dd7bd0c8a4 Prevent log spam when WS subscribe_condition is active (#172832) 2026-06-03 10:10:30 +02:00
Petro31 c462a1c188 Add translations for template device trackers in_zones option (#172850) 2026-06-03 08:38:18 +02:00
fdebrus 96c5110b7e Vistapool: flip docs-related quality-scale rules to done (#172827)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-03 08:04:52 +02:00
Colin 64e8ed2737 Add missing translation keys to openevse (#172802) 2026-06-03 08:03:30 +02:00
renovate[bot] 4171d092f7 Update coverage to 7.14.1 (#172878)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-03 07:59:49 +02:00
J. Nick Koston 7af867ad4d Avoid double-decoding websocket_api TEXT frames with decode_text (#172891) 2026-06-03 07:52:02 +02:00
Paulus Schoutsen 907fe40304 Regenerate mdi_icons.py for frontend 20260527.3 (#172887) 2026-06-03 05:56:21 +02:00
Franck Nijhof af08e5e7d0 Bump version to 2026.6.0b2 2026-06-01 21:05:58 +00:00
Franck Nijhof b03d87dc21 Cancel iCloud polling timer on config entry unload (#172793) 2026-06-01 21:05:46 +00:00
Tom d8a9ea1d9d Fix ProxmoxVE missing unused token data (#172782) 2026-06-01 21:05:44 +00:00
J. Nick Koston 5ff07fcc49 Explain why a Snooz device could not be found (#172780) 2026-06-01 21:05:42 +00:00
J. Nick Koston 6f59bb0661 Explain why an LD2410 BLE device could not be found (#172779) 2026-06-01 21:05:40 +00:00
J. Nick Koston c82d32bbae Explain why a Husqvarna Automower BLE device could not be connected to (#172774) 2026-06-01 21:05:38 +00:00
Ingo Fischer 4fbc363965 Filter stale replayed BLE advertisements in Matter BLE proxy (#172773)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:05:36 +00:00
J. Nick Koston 8622f0f4de Explain why an eQ-3 Bluetooth device could not be found (#172770) 2026-06-01 21:05:34 +00:00
J. Nick Koston b49a6b89b6 Bump habluetooth to 6.8.1 (#172768) 2026-06-01 21:05:32 +00:00
J. Nick Koston 0bfd4c44bb Explain why a LED BLE device could not be found (#172764) 2026-06-01 21:05:30 +00:00
J. Nick Koston c09216650f Explain why an INKBIRD device could not be found (#172762) 2026-06-01 21:05:28 +00:00
J. Nick Koston 6057d32636 Explain why a Yale Access Bluetooth device could not be found (#172761) 2026-06-01 21:05:26 +00:00
Bram Kragten 51c9d0c6e5 Bump frontend to 20260527.2 (#172759)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-06-01 21:05:24 +00:00
J. Nick Koston 323304664e Explain why an Airthings BLE device could not be found (#172758) 2026-06-01 21:05:22 +00:00
A. Gideonse 3dda7d9848 Fix binary sensor defaults for Indevolt (#172714) 2026-06-01 21:05:20 +00:00
A. Gideonse 5e56d74257 Bump indevolt-api to 1.8.3 (#172683) 2026-06-01 21:05:18 +00:00
Thijs W. e5f9c7892a Fix get_play_status function call in frontier silicon (#172705) 2026-06-01 21:01:29 +00:00
Michael a0d713a4a7 Use proper user-agent to fetch feeds (#172655) 2026-06-01 21:01:27 +00:00
jameson_uk 84f4f876b1 media_player platform fixes for Alexa Devices (#172611) 2026-06-01 21:01:25 +00:00
Franck Nijhof 7b06228a5a Bump version to 2026.6.0b1 2026-06-01 16:54:56 +00:00
Paul Bottein 06b2ec22f0 Bump yoto-api to 3.1.5 (#172753) 2026-06-01 16:54:33 +00:00
jameson_uk 7950998083 Bump aioamazondevices to 13.8.2 (#172748) 2026-06-01 16:54:30 +00:00
Maciej Bieniek 86999063d7 Translate the name of the Tractive tracker (#172747) 2026-06-01 16:54:28 +00:00
Maciej Bieniek 9843fdad2c Add missing _attr_name = None for Tractive device tracker (#172746) 2026-06-01 16:54:26 +00:00
Jan Bouwhuis e53914a0ef Fix MQTT device_tracker logging attributes order (#172732) 2026-06-01 16:54:24 +00:00
Franck Nijhof f7afe22318 Skip Overkiz events for unknown device URLs (#172712) 2026-06-01 16:54:22 +00:00
Franck Nijhof acfecd7f5c Convert set_id to int in LG TV RS-232 config flow (#172701) 2026-06-01 16:54:20 +00:00
Franck Nijhof 56057a11e6 Return 404 instead of 500 when media player artwork is unavailable (#172700) 2026-06-01 16:54:18 +00:00
Yardian Support 0d079c57e4 Fix Yardian water hammer diagnostic sensor name (#172698) 2026-06-01 16:54:16 +00:00
Denis Shulyaka 3ad3e1fafb Fix ai_task camera snapshot mime type (#172682) 2026-06-01 16:54:13 +00:00
Josef Zweck 0677ed824f Fix tedee entity availability (#172667)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-01 16:54:11 +00:00
Jordan Harvey 4b9945e012 Bump pynintendoparental to 2.4.0 (#172666) 2026-06-01 16:54:08 +00:00
Michael 9fa0132b1c Add missing exception translation keys in Ecovacs (#172658) 2026-06-01 16:54:06 +00:00
jameson_uk 10a25368a0 Improve http2 task handling for Alexa Devices (#172649) 2026-06-01 16:54:04 +00:00
epenet fbb68c26b6 Bump tuya-device-handlers to 0.0.22 (#172648) 2026-06-01 16:54:02 +00:00
Michael 25875de414 Add extra device info to FRITZ!Box Tools diagnostics (#172647) 2026-06-01 16:54:00 +00:00
TheJulianJES 22ace88b2c Bump ZHA to 1.4.1 (#172640) 2026-06-01 16:53:57 +00:00
David Knowles a47105d314 Schlage: use lock connected status as availability signal (#172638)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-01 16:53:55 +00:00
Jan Bouwhuis b50bfda00c Fix MQTT device_tracker not saving state on location accuracy changes (#172629) 2026-06-01 16:53:53 +00:00
Sören 0d37319ba9 Improve Avea Bluetooth discovery flow (#172623) 2026-06-01 16:53:51 +00:00
Michael 24a5c75cf2 Show error about missing api permissions while browsing Immich media (#172609) 2026-06-01 16:53:49 +00:00
renovate[bot] dd43b1135d Update rf-protocols to 4.0.1 (#172597) 2026-06-01 16:53:47 +00:00
J. Nick Koston de0a202c4e Explain why a Switchbot device could not be found (#172581) 2026-06-01 16:53:44 +00:00
J. Nick Koston d550d1da90 Expose bluetooth address reachability diagnostics API (#172578) 2026-06-01 16:53:42 +00:00
J. Nick Koston ce8875ae8c Bump habluetooth to 6.8.0 (#172577) 2026-06-01 16:53:40 +00:00
J. Nick Koston 3364096b2b Fix ESPHome update entity stuck on for project versions with build suffix (#172571) 2026-06-01 16:53:38 +00:00
A. Gideonse c2b75b9634 Bugfix: Gen-1 Inverter sensor for Indevolt to display "N/A" when turned off (#172559) 2026-06-01 16:53:36 +00:00
Franck Nijhof ae278d3c80 Sanitize surrogate characters in MeteoAlarm alert attributes (#172545) 2026-06-01 16:53:34 +00:00
Paul Bottein 25f9cd9ab8 Fix Yoto OAuth flow with cloud credentials (#172544)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-01 16:53:31 +00:00
Franck Nijhof 796d82d6ed Add missing ssdp dependency to BraviaTV manifest (#172536) 2026-06-01 16:53:30 +00:00
Franck Nijhof 4b517fb164 Use state-based icon for Hue grouped light (#172535) 2026-06-01 16:53:27 +00:00
Kamil Breguła 2d74091a36 Refresh WLED firmware releases on manual entity update (#172517)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-01 16:53:25 +00:00
Franck Nijhof 504e22ee3e Raise errors instead of swallowing exceptions in Toon action handlers (#172511) 2026-06-01 16:53:23 +00:00
Franck Nijhof c95a39c26e Guard Shelly repairs checks for uninitialized RPC devices (#172509) 2026-06-01 16:53:21 +00:00
Franck Nijhof 8ec3eac705 Fix Overkiz UnoIO cover reporting wrong movement direction (#172506) 2026-06-01 16:53:19 +00:00
Franck Nijhof 589d2637c9 Fix ephember crash when zone mode is None (#172504) 2026-06-01 16:53:17 +00:00
Franck Nijhof 26cf728165 Handle missing notAfter field in cert_expiry certificate data (#172503)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-01 16:51:22 +00:00
Franck Nijhof b61559bdbb Handle malformed response errors in Denon AVR error wrapper (#172502) 2026-06-01 16:06:02 +00:00
Jan Bouwhuis 57259132d9 Silent migrate MQTT protocol version to version 5 if the broker supports it or raise an issue (#172500)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-01 16:06:00 +00:00
Franck Nijhof 2776e966ff Reduce Wyoming satellite disconnect log to debug level (#172499) 2026-06-01 16:05:58 +00:00
Franck Nijhof 5f9872886d Convert Roomba hw_version to string for device registry (#172497) 2026-06-01 16:05:56 +00:00
Franck Nijhof f728a1bf09 Add missing Flexit BACnet transient operation modes to preset map (#172493) 2026-06-01 16:05:53 +00:00
Franck Nijhof df65132268 Add prog operating mode to Overkiz Atlantic heater HVAC mapping (#172491) 2026-06-01 16:05:51 +00:00
Michael c13822b776 Handle FileNotFoundError in Immich upload_file action (#172490) 2026-06-01 16:05:49 +00:00
Simone Chemelli c6d696db0c Remove redundant definitions in Alexa Devices (#172488) 2026-06-01 16:05:46 +00:00
Franck Nijhof 114c9bbafa Increase ConfigEntryNotReady retry backoff cap from 80s to 10 minutes (#172487) 2026-06-01 16:05:44 +00:00
Franck Nijhof 323ce99fda Fix Tado config flow crash on device activation polling (#172486) 2026-06-01 16:05:42 +00:00
Jan Bouwhuis 7a7ef85db2 Move MQTT protocol setting to main options (#172482) 2026-06-01 16:05:40 +00:00
Franck Nijhof 7ab402618d Handle DAVError in CalDAV get_supported_components (#172479) 2026-06-01 16:05:37 +00:00
Franck Nijhof aa87295a1e Fix Growatt setup failure on API rate limit (#172472) 2026-06-01 16:05:35 +00:00
Simone Chemelli 3bd979e976 Bump samsungtvws to 3.0.5 (#172471) 2026-06-01 16:05:33 +00:00
Paul Bottein 9dddf76548 Name the Broadlink RF transmitter entity (#172468) 2026-06-01 16:05:31 +00:00
Franck Nijhof 1828579f03 Fix Volvo lock crash when API field is missing from coordinator data (#172465) 2026-06-01 16:05:29 +00:00
Bram Kragten 47bca8d8c2 Bump frontend to 20260527.1 (#172462)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-01 16:05:27 +00:00
Paulus Schoutsen 6f3fb5c7bd Add lg_tv_rs232 to LG brand (#172458)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-01 16:05:24 +00:00
TheJulianJES d9b4b5b3d0 Fix Matter BLE proxy blocking startup (#172456) 2026-06-01 16:05:22 +00:00
Ronald van der Meer 342b364af6 Fix Duco regression where entities become unavailable when LAN info fetch fails (#172448) 2026-06-01 16:05:20 +00:00
Simone Chemelli 951cd71741 Discard old events for Alexa Devices (#172446) 2026-06-01 16:05:18 +00:00
Franck Nijhof e86a54f81c Fix Hue light ZeroDivisionError when mirek value is zero (#172442) 2026-06-01 16:05:15 +00:00
Simone Chemelli ba8b33e1a9 Fix Shelly sensor restore when not initialized (#172441) 2026-06-01 16:05:13 +00:00
Franck Nijhof b6c40ba3fc Fix Jellyfin media source crash when entry is not loaded (#172437) 2026-06-01 16:05:11 +00:00
Franck Nijhof f2f29c07c7 Fix SmartThings light checking wrong component for capabilities (#172430)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-01 16:05:08 +00:00
Franck Nijhof 50a3ab115d Fix iZone integration broken by python-izone 1.2.10 API change (#172427) 2026-06-01 16:05:06 +00:00
Franck Nijhof c204054847 Convert yamaha_musiccast sw_version to string (#172411) 2026-06-01 16:05:04 +00:00
Jan Bouwhuis 28d6eab2dd Improve MQTT protocol deprecation repair message (#172404)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-01 16:05:02 +00:00
Manu 6b1ee57bd5 Fix index error in DuckDNS integration (#172392) 2026-06-01 16:05:00 +00:00
J. Nick Koston 7247f95b05 Bump onvif-zeep-async to 4.1.1 (#172391) 2026-06-01 16:04:57 +00:00
J. Nick Koston cdeafdfd42 Bump yalexs to 9.2.1 (#172389) 2026-06-01 16:04:55 +00:00
Abílio Costa 9d60fce72e Fix OMIE sensors not updating on setup (#172383) 2026-06-01 16:04:53 +00:00
Simone Chemelli 2e4c6c4370 Bump aioamazondevices to 13.8.1 (#172382) 2026-06-01 16:04:50 +00:00
J. Nick Koston b7e36e297b Bump dbus-fast to 5.0.16 (#172378) 2026-06-01 16:04:48 +00:00
Stefan Agner 7e178efe63 Reject backup uploads with unsafe inner name (#172368)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 16:04:46 +00:00
puddly 38f25c4b41 Bump ZHA to 1.4.0 (#172357) 2026-06-01 16:04:44 +00:00
torben-iometer 2c2e70a11c bump iometer version to 1.0.1 (#172338) 2026-06-01 16:04:41 +00:00
Linkplay2020 190350aec3 Bump wiim to 1.0.4 (#172334)
Co-authored-by: Tao Jiang <tao.jiang@linkplay.com>
2026-06-01 16:04:39 +00:00
tlpeter a87083b6c1 Bump renault-api to 0.5.11 (#172333)
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-06-01 16:04:36 +00:00
Mike Degatano d5be54fd40 Migrate analytics integration to config entry setup (#171801)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-01 16:04:34 +00:00
Mike Degatano 46f2ad9eb2 During onboarding, ensure Supervisor is up to date during hassio setup (#171129)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 16:04:31 +00:00
mhuiskes add75622d6 Fix zeversolar coordinator to raise UpdateFailed on errors (#170507) 2026-06-01 16:04:29 +00:00
Daniel Feinberg 2f334d657d Fix apple_tv HomePod streaming failures when device is idle (#170033)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 16:04:26 +00:00
Nikhil Deepak fd69d384be Reset MQTT valve opening/closing state at intermediate positions (#165176)
Co-authored-by: jbouwh <jan@jbsoft.nl>
2026-06-01 16:04:24 +00:00
Franck Nijhof fce17c8e6f Bump version to 2026.6.0b0 2026-05-27 16:07:37 +00:00
748 changed files with 18095 additions and 5598 deletions
+2 -33
View File
@@ -8,39 +8,8 @@ description: Reviews GitHub pull requests and provides feedback comments. This i
## Follow these steps:
1. Use 'gh pr view' to get the PR details and description.
2. Use 'gh pr diff' to see all the changes in the PR.
3. Analyze the code changes for:
- Code quality and style consistency
- Potential bugs or issues
- Performance implications
- Security concerns
- Test coverage
- Documentation updates if needed
4. Ensure any existing review comments have been addressed.
5. Generate constructive review comments in the CONSOLE. DO NOT POST TO GITHUB YOURSELF.
## Verification:
- After the review, run parallel subagents for each finding to double check it.
- Spawn up to a maximum of 10 parallel subagents at a time.
- Gather the results from the subagents and summarize them in the final review comments.
3. Review the changes following the `review` skill. It is VERY IMPORTANT to follow the `review` skill instructions.
4. Check if all existing review comments have been addressed.
## IMPORTANT:
- Just review. DO NOT make any changes
- Be constructive and specific in your comments
- Suggest improvements where appropriate
- Only provide review feedback in the CONSOLE. DO NOT ACT ON GITHUB.
- No need to run tests or linters, just review the code changes.
- No need to highlight things that are already good.
## Output format:
- List specific comments for each file/line that needs attention.
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
- Example output:
```
Overall assessment: request changes.
- [CRITICAL] sensor.py:143 - Memory leak
- [PROBLEM] data_processing.py:87 - Inefficient algorithm
- [SUGGESTION] test_init.py:45 - Improve x variable name
```
- Make sure to include the file and line number when possible in the bullet points.
+38
View File
@@ -0,0 +1,38 @@
---
name: review
description: Reviews code changes and provides constructive feedback. Should be used when a review is requested to provide a consistent review behavior and output format. This skill can be used for code reviews in general, not just for GitHub pull requests.
---
# Review Code Changes
## Analyze the code changes for:
- Code quality and style consistency
- Potential bugs or issues
- Performance implications
- Security concerns
- Test coverage
- Documentation updates if needed
## Verification:
- After the review, run parallel subagents for each finding to double-check it.
- Spawn up to a maximum of 10 parallel subagents at a time.
- Gather the results from the subagents and summarize them in the final review comments.
## IMPORTANT:
- Just review. DO NOT make any changes.
- Be constructive and specific in your comments.
- Suggest improvements where appropriate.
- No need to run tests or linters, just review the code changes.
- No need to highlight things that are already good.
## Output format:
- List specific comments for each file/line that needs attention.
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
- Example output:
```
Overall assessment: request changes.
- [CRITICAL] sensor.py:143 - Memory leak
- [PROBLEM] data_processing.py:87 - Inefficient algorithm
- [SUGGESTION] test_init.py:45 - Improve x variable name
```
- Make sure to include the file and line number when possible in the bullet points.
+7 -7
View File
@@ -36,7 +36,7 @@
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
# - github/gh-aw-actions/setup@029b1de3e913e0604df1ada9da1cec03e282c4db # v0.76.0
# - github/gh-aw-actions/setup@97f280b14527ca95859c0facba201aeccb2c097f # v0.77.3
#
# Container images used:
# - ghcr.io/github/gh-aw-firewall/agent:0.25.46
@@ -90,7 +90,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@029b1de3e913e0604df1ada9da1cec03e282c4db # v0.76.0
uses: github/gh-aw-actions/setup@97f280b14527ca95859c0facba201aeccb2c097f # v0.77.3
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -352,7 +352,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@029b1de3e913e0604df1ada9da1cec03e282c4db # v0.76.0
uses: github/gh-aw-actions/setup@97f280b14527ca95859c0facba201aeccb2c097f # v0.77.3
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -961,7 +961,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@029b1de3e913e0604df1ada9da1cec03e282c4db # v0.76.0
uses: github/gh-aw-actions/setup@97f280b14527ca95859c0facba201aeccb2c097f # v0.77.3
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1100,7 +1100,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@029b1de3e913e0604df1ada9da1cec03e282c4db # v0.76.0
uses: github/gh-aw-actions/setup@97f280b14527ca95859c0facba201aeccb2c097f # v0.77.3
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1325,7 +1325,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@029b1de3e913e0604df1ada9da1cec03e282c4db # v0.76.0
uses: github/gh-aw-actions/setup@97f280b14527ca95859c0facba201aeccb2c097f # v0.77.3
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1383,7 +1383,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@029b1de3e913e0604df1ada9da1cec03e282c4db # v0.76.0
uses: github/gh-aw-actions/setup@97f280b14527ca95859c0facba201aeccb2c097f # v0.77.3
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -236,7 +236,7 @@ jobs:
- name: Detect duplicates using AI
id: ai_detection
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0
uses: actions/ai-inference@a7805884c80886efc241e94a5351df715968a0ad # v2.1.1
with:
model: openai/gpt-4o
system-prompt: |
@@ -62,7 +62,7 @@ jobs:
- name: Detect language using AI
id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/ai-inference@17ff458cb182449bbb2e43701fcd98f6af8f6570 # v2.1.0
uses: actions/ai-inference@a7805884c80886efc241e94a5351df715968a0ad # v2.1.1
with:
model: openai/gpt-4o-mini
system-prompt: |
+2 -2
View File
@@ -137,7 +137,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
uses: home-assistant/wheels@34957438948e0b3dcde73c77750643dadae594f5 # 2026.06.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -195,7 +195,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
uses: home-assistant/wheels@34957438948e0b3dcde73c77750643dadae594f5 # 2026.06.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
+1 -1
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.14
rev: v0.15.15
hooks:
- id: ruff-check
args:
Generated
+6 -2
View File
@@ -453,6 +453,8 @@ CLAUDE.md @home-assistant/core
/tests/components/ecovacs/ @mib1185 @edenhaus @Augar
/homeassistant/components/ecowitt/ @pvizeli
/tests/components/ecowitt/ @pvizeli
/homeassistant/components/edifier_infrared/ @abmantis
/tests/components/edifier_infrared/ @abmantis
/homeassistant/components/efergy/ @tkdrob
/tests/components/efergy/ @tkdrob
/homeassistant/components/egardia/ @jeroenterheerdt
@@ -625,8 +627,8 @@ CLAUDE.md @home-assistant/core
/tests/components/generic_hygrostat/ @Shulyaka
/homeassistant/components/geniushub/ @manzanotti
/tests/components/geniushub/ @manzanotti
/homeassistant/components/gentex_homelink/ @niaexa @ryanjones-gentex
/tests/components/gentex_homelink/ @niaexa @ryanjones-gentex
/homeassistant/components/gentex_homelink/ @Gentex-Corporation/Homelink @rjones-gentex
/tests/components/gentex_homelink/ @Gentex-Corporation/Homelink @rjones-gentex
/homeassistant/components/geo_json_events/ @exxamalte
/tests/components/geo_json_events/ @exxamalte
/homeassistant/components/geo_location/ @home-assistant/core
@@ -840,6 +842,8 @@ CLAUDE.md @home-assistant/core
/tests/components/imgw_pib/ @bieniu
/homeassistant/components/immich/ @mib1185
/tests/components/immich/ @mib1185
/homeassistant/components/imou/ @Imou-OpenPlatform
/tests/components/imou/ @Imou-OpenPlatform
/homeassistant/components/improv_ble/ @emontnemery
/tests/components/improv_ble/ @emontnemery
/homeassistant/components/incomfort/ @jbouwh
@@ -13,5 +13,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["actron-neo-api==0.5.6"]
"requirements": ["actron-neo-api==0.5.12"]
}
+1 -1
View File
@@ -8,5 +8,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["airos==0.6.5"]
"requirements": ["airos==0.6.8"]
}
@@ -1,6 +1,10 @@
"""The AirVisual Pro integration."""
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -25,6 +29,12 @@ class AirVisualProEntity(CoordinatorEntity[AirVisualProCoordinator]):
"""Return device registry information for this entity."""
return DeviceInfo(
identifiers={(DOMAIN, self.coordinator.data["serial_number"])},
connections={
(
CONNECTION_NETWORK_MAC,
format_mac(self.coordinator.data["status"]["mac_address"]),
)
},
manufacturer="AirVisual",
model=self.coordinator.data["status"]["model"],
name=self.coordinator.data["settings"]["node_name"],
+5 -4
View File
@@ -59,7 +59,6 @@ ATTR_EXTERNAL_URL = "external_url"
ATTR_INTERNAL_URL = "internal_url"
ATTR_LOCATION_NAME = "location_name"
ATTR_INSTALLATION_TYPE = "installation_type"
ATTR_REQUIRES_API_PASSWORD = "requires_api_password"
ATTR_UUID = "uuid"
ATTR_VERSION = "version"
@@ -222,7 +221,7 @@ class APIStatesView(HomeAssistantView):
states = (
state.as_dict_json
for state in hass.states.async_all()
if entity_perm(state.entity_id, "read")
if entity_perm(state.entity_id, POLICY_READ)
)
response = web.Response(
body=b"".join((b"[", b",".join(states), b"]")),
@@ -294,8 +293,10 @@ class APIEntityStateView(HomeAssistantView):
# Read the state back for our response
status_code = HTTPStatus.CREATED if is_new_state else HTTPStatus.OK
state = hass.states.get(entity_id)
assert state
if (state := hass.states.get(entity_id)) is None:
return self.json_message(
"Error storing state.", HTTPStatus.INTERNAL_SERVER_ERROR
)
resp = self.json(state.as_dict(), status_code)
resp.headers.add("Location", f"/api/states/{entity_id}")
@@ -5,6 +5,8 @@ import logging
from pathlib import Path
from typing import Any
from hassil.parse_expression import parse_sentence
from hassil.parser import ParseError
from hassil.util import (
PUNCTUATION_END,
PUNCTUATION_END_WORD,
@@ -164,6 +166,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
[cv.string],
has_one_non_empty_item,
has_no_punctuation,
is_valid_sentence,
),
}
],
@@ -212,6 +215,17 @@ def has_no_punctuation(value: list[str]) -> list[str]:
return value
def is_valid_sentence(value: list[str]) -> list[str]:
"""Validate result can be parsed by hassil."""
for sentence in value:
try:
parse_sentence(sentence)
except ParseError as err:
raise vol.Invalid(f"invalid sentence: {err}") from err
return value
def has_one_non_empty_item(value: list[str]) -> list[str]:
"""Validate result has at least one item."""
if len(value) < 1:
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.5.0"]
"requirements": ["hassil==3.6.0"]
}
+19 -5
View File
@@ -2,12 +2,18 @@
import avea
from homeassistant.components.bluetooth import async_ble_device_from_address
from homeassistant.components.bluetooth import (
BluetoothReachabilityIntent,
async_address_reachability_diagnostics,
async_ble_device_from_address,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
type AveaConfigEntry = ConfigEntry[avea.Bulb]
PLATFORMS: list[Platform] = [Platform.LIGHT]
@@ -15,12 +21,20 @@ PLATFORMS: list[Platform] = [Platform.LIGHT]
async def async_setup_entry(hass: HomeAssistant, entry: AveaConfigEntry) -> bool:
"""Set up Avea from a config entry."""
ble_device = async_ble_device_from_address(
hass, entry.data[CONF_ADDRESS], connectable=True
)
address = entry.data[CONF_ADDRESS]
ble_device = async_ble_device_from_address(hass, address, connectable=True)
if not ble_device:
raise ConfigEntryNotReady(
f"Could not find Avea device with address {entry.data[CONF_ADDRESS]}"
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={
"address": address,
"reason": async_address_reachability_diagnostics(
hass,
address.upper(),
BluetoothReachabilityIntent.CONNECTION,
),
},
)
entry.runtime_data = avea.Bulb(ble_device)
@@ -22,6 +22,11 @@
}
}
},
"exceptions": {
"device_not_found": {
"message": "Could not find Avea device with address {address}: {reason}"
}
},
"issues": {
"deprecated_yaml": {
"description": "[%key:component::homeassistant::issues::deprecated_yaml::description%]",
@@ -0,0 +1 @@
"""Virtual integration: Avosdim."""
@@ -0,0 +1,6 @@
{
"domain": "avosdim",
"name": "Avosdim",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}
@@ -21,6 +21,10 @@ BINARY_SENSOR_TYPES = (
key="moisture",
device_class=BinarySensorDeviceClass.MOISTURE,
),
BinarySensorEntityDescription(
key="open",
device_class=BinarySensorDeviceClass.WINDOW,
),
)
+20 -16
View File
@@ -2,7 +2,7 @@
import blebox_uniapi.button
from homeassistant.components.button import ButtonEntity
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -13,6 +13,16 @@ from .util import blebox_command
PARALLEL_UPDATES = 1
BUTTON_TYPES: dict[str, ButtonEntityDescription] = {
"up": ButtonEntityDescription(key="up", translation_key="up"),
"down": ButtonEntityDescription(key="down", translation_key="down"),
"fav": ButtonEntityDescription(key="fav", translation_key="fav"),
"open": ButtonEntityDescription(key="open", translation_key="open"),
"close": ButtonEntityDescription(key="close", translation_key="close"),
}
_DEFAULT_BUTTON = ButtonEntityDescription(key="button")
async def async_setup_entry(
hass: HomeAssistant,
@@ -35,22 +45,16 @@ class BleBoxButtonEntity(BleBoxEntity[blebox_uniapi.button.Button], ButtonEntity
self, coordinator: BleBoxCoordinator, feature: blebox_uniapi.button.Button
) -> None:
"""Initialize a BleBox button feature."""
super().__init__(coordinator, feature)
self._attr_icon = self.get_icon()
def get_icon(self) -> str | None:
"""Return icon for endpoint."""
if "up" in self._feature.query_string:
return "mdi:arrow-up-circle"
if "down" in self._feature.query_string:
return "mdi:arrow-down-circle"
if "fav" in self._feature.query_string:
return "mdi:heart-circle"
if "open" in self._feature.query_string:
return "mdi:arrow-up-circle"
if "close" in self._feature.query_string:
return "mdi:arrow-down-circle"
return None
super().__init__(coordinator, feature)
self.entity_description = self._get_description()
def _get_description(self) -> ButtonEntityDescription:
"""Return the description matching this button's query string."""
for key, description in BUTTON_TYPES.items():
if key in self._feature.query_string:
return description
return _DEFAULT_BUTTON
@blebox_command
async def async_press(self) -> None:
+7
View File
@@ -14,6 +14,13 @@ UNKNOWN = "unknown"
DEFAULT_HOST = "192.168.0.2"
DEFAULT_PORT = 80
OPEN_STATUS: dict[int, str] = {
0: "open",
1: "unclosed_or_unlocked",
2: "ajar",
3: "closed_but_unlocked",
4: "closed",
}
LIGHT_MAX_KELVINS = 6500 # 154 Mireds
LIGHT_MIN_KELVINS = 2700 # 370 Mireds
@@ -43,6 +43,6 @@ class BleBoxCoordinator(DataUpdateCoordinator[None]):
except Error as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_key="data_update_failed",
translation_placeholders={"error": str(err)},
) from err
@@ -0,0 +1,33 @@
"""Diagnostics support for BleBox devices."""
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from . import BleBoxConfigEntry
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: BleBoxConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
product = entry.runtime_data.box
return {
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
"device": {
"name": product.name,
"type": product.type,
"model": product.model,
"unique_id": product.unique_id,
"firmware_version": product.firmware_version,
"hardware_version": product.hardware_version,
"available_firmware_version": product.available_firmware_version,
"api_version": product.api_version,
"last_data": product.last_data,
},
}
@@ -0,0 +1,26 @@
{
"entity": {
"button": {
"close": {
"default": "mdi:arrow-down-circle"
},
"down": {
"default": "mdi:arrow-down-circle"
},
"fav": {
"default": "mdi:heart-circle"
},
"open": {
"default": "mdi:arrow-up-circle"
},
"up": {
"default": "mdi:arrow-up-circle"
}
},
"sensor": {
"power_consumption": {
"default": "mdi:lightning-bolt"
}
}
}
}
+10 -6
View File
@@ -19,10 +19,11 @@ from homeassistant.components.light import (
LightEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BleBoxConfigEntry
from .const import LIGHT_MAX_KELVINS, LIGHT_MIN_KELVINS
from .const import DOMAIN, LIGHT_MAX_KELVINS, LIGHT_MIN_KELVINS
from .coordinator import BleBoxCoordinator
from .entity import BleBoxEntity
from .util import blebox_command
@@ -215,8 +216,10 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
try:
await self._feature.async_on(value)
except ValueError as exc:
raise ValueError(
f"Turning on '{self.name}' failed: Bad value {value}"
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="bad_value",
translation_placeholders={"error": str(exc)},
) from exc
if effect is not None:
@@ -224,9 +227,10 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
effect_value = self.effect_list.index(effect)
await self._feature.async_api_command("effect", effect_value)
except ValueError as exc:
raise ValueError(
f"Turning on with effect '{self.name}' failed: {effect} not in"
" effect list."
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="effect_not_found",
translation_placeholders={"error": str(exc)},
) from exc
@blebox_command
+42 -21
View File
@@ -1,5 +1,7 @@
"""BleBox sensor entities."""
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
import blebox_uniapi.sensor
@@ -26,96 +28,113 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import BleBoxConfigEntry
from .const import OPEN_STATUS
from .coordinator import BleBoxCoordinator
from .entity import BleBoxEntity
PARALLEL_UPDATES = 0
SENSOR_TYPES = (
SensorEntityDescription(
@dataclass(kw_only=True, frozen=True)
class BleBoxSensorEntityDescription(SensorEntityDescription):
"""Describes a BleBox sensor entity."""
value_fn: Callable[[StateType], StateType] = lambda v: v
SENSOR_TYPES: tuple[BleBoxSensorEntityDescription, ...] = (
BleBoxSensorEntityDescription(
key="pm1",
device_class=SensorDeviceClass.PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="pm2_5",
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="pm10",
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="powerConsumption",
translation_key="power_consumption",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_display_precision=2,
icon="mdi:lightning-bolt",
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="wind",
device_class=SensorDeviceClass.WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="illuminance",
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="forwardActiveEnergy",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="reverseActiveEnergy",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="reactivePower",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="activePower",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="apparentPower",
device_class=SensorDeviceClass.APPARENT_POWER,
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="voltage",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="current",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
),
SensorEntityDescription(
BleBoxSensorEntityDescription(
key="frequency",
device_class=SensorDeviceClass.FREQUENCY,
native_unit_of_measurement=UnitOfFrequency.HERTZ,
),
BleBoxSensorEntityDescription(
key="openStatus",
translation_key="open_status",
device_class=SensorDeviceClass.ENUM,
icon="mdi:window-open",
options=list(OPEN_STATUS.values()),
value_fn=lambda v: OPEN_STATUS.get(int(v)) if v is not None else None,
),
)
@@ -138,20 +157,22 @@ async def async_setup_entry(
class BleBoxSensorEntity(BleBoxEntity[blebox_uniapi.sensor.BaseSensor], SensorEntity):
"""Representation of a BleBox sensor feature."""
entity_description: BleBoxSensorEntityDescription
def __init__(
self,
coordinator: BleBoxCoordinator,
feature: blebox_uniapi.sensor.BaseSensor,
description: SensorEntityDescription,
description: BleBoxSensorEntityDescription,
) -> None:
"""Initialize a BleBox sensor feature."""
super().__init__(coordinator, feature)
self.entity_description = description
@property
def native_value(self):
def native_value(self) -> StateType:
"""Return the state."""
return self._feature.native_value
return self.entity_description.value_fn(self._feature.native_value)
@property
def last_reset(self) -> datetime | None:
+29 -1
View File
@@ -35,9 +35,37 @@
}
}
},
"entity": {
"sensor": {
"open_status": {
"state": {
"ajar": "Ajar",
"closed": "[%key:common::state::closed%]",
"closed_but_unlocked": "Closed but unlocked",
"open": "[%key:common::state::open%]",
"unclosed_or_unlocked": "Unclosed or unlocked"
}
}
}
},
"exceptions": {
"update_failed": {
"bad_value": {
"message": "Turning on the light failed: {error}"
},
"command_failed": {
"message": "Failed to execute command on the BleBox device: {error}"
},
"data_update_failed": {
"message": "An error occurred while communicating with the BleBox device: {error}"
},
"effect_not_found": {
"message": "The specified light effect is not available on this device: {error}"
},
"install_failed": {
"message": "Failed to install firmware update on the BleBox device: {error}"
},
"update_failed": {
"message": "Failed to fetch firmware update information from the BleBox device: {error}"
}
}
}
+11 -2
View File
@@ -18,6 +18,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_call_later
from . import BleBoxConfigEntry
from .const import DOMAIN
from .coordinator import BleBoxCoordinator
from .entity import BleBoxEntity
@@ -86,7 +87,11 @@ class BleBoxUpdateEntity(BleBoxEntity[blebox_uniapi.update.Update], UpdateEntity
try:
await self._feature.async_update()
except Error as ex:
raise HomeAssistantError(ex) from ex
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": str(ex)},
) from ex
self._sync_sw_version()
@property
@@ -121,7 +126,11 @@ class BleBoxUpdateEntity(BleBoxEntity[blebox_uniapi.update.Update], UpdateEntity
await self._feature.async_install()
except Error as ex:
self._reset_progress()
raise HomeAssistantError(ex) from ex
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="install_failed",
translation_placeholders={"error": str(ex)},
) from ex
self._poll_cancel = async_call_later(
self.hass, _POLL_INTERVAL_SECONDS, self._poll_until_updated
)
+6 -1
View File
@@ -7,6 +7,7 @@ from blebox_uniapi.error import Error
from homeassistant.exceptions import HomeAssistantError
from .const import DOMAIN
from .entity import BleBoxEntity
@@ -22,7 +23,11 @@ def blebox_command[_BleBoxEntityT: BleBoxEntity, **_P, _R](
try:
return await func(self, *args, **kwargs)
except Error as err:
raise HomeAssistantError(str(err)) from err
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_failed",
translation_placeholders={"error": str(err)},
) from err
finally:
await self.coordinator.async_refresh()
+12 -6
View File
@@ -6,6 +6,7 @@ These APIs are the only documented way to interact with the bluetooth integratio
import asyncio
from asyncio import Future
from collections.abc import Callable, Iterable
from contextlib import ExitStack
from typing import TYPE_CHECKING, cast
from bleak import BleakScanner
@@ -178,15 +179,20 @@ async def async_process_advertisements(
if not done.done() and callback(service_info):
done.set_result(service_info)
unload = _get_manager(hass).async_register_callback(
_async_discovered_device, match_dict, mode, scan_duration=timeout
)
manager = _get_manager(hass)
with ExitStack() as stack:
unload = manager.async_register_callback(
_async_discovered_device, match_dict, mode
)
stack.callback(unload)
if mode == BluetoothScanningMode.ACTIVE:
task = hass.async_create_task(manager.async_request_active_scan(timeout))
stack.callback(task.cancel)
try:
async with asyncio.timeout(timeout):
return await done
finally:
unload()
@hass_callback
@@ -21,6 +21,6 @@
"bluetooth-auto-recovery==1.6.4",
"bluetooth-data-tools==1.29.18",
"dbus-fast==5.0.16",
"habluetooth==6.8.1"
"habluetooth==6.8.3"
]
}
+10 -18
View File
@@ -1,18 +1,19 @@
"""The Brands integration."""
from collections import deque
from collections.abc import Container, Mapping
from http import HTTPStatus
import logging
from pathlib import Path
from random import SystemRandom
import time
from typing import Any, Final
from typing import Any, Final, override
from aiohttp import ClientError, hdrs, web
from aiohttp import ClientError, web
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant, callback, valid_domain
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -108,23 +109,18 @@ def _read_brand_file(brand_dir: Path, image: str) -> bytes | None:
class _BrandsBaseView(HomeAssistantView):
"""Base view for serving brand images."""
requires_auth = False
use_query_token_for_auth = True
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the view."""
self._hass = hass
self._cache_dir = Path(hass.config.cache_path(DOMAIN))
def _authenticate(self, request: web.Request) -> None:
"""Authenticate the request using Bearer token or query token."""
access_tokens: deque[str] = self._hass.data[DOMAIN]
authenticated = (
request[KEY_AUTHENTICATED] or request.query.get("token") in access_tokens
)
if not authenticated:
if hdrs.AUTHORIZATION in request.headers:
raise web.HTTPUnauthorized
raise web.HTTPForbidden
@callback
@override
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
"""Return valid auth tokens, which can be used for query token authentication."""
return self._hass.data[DOMAIN]
async def _serve_from_custom_integration(
self,
@@ -240,8 +236,6 @@ class BrandsIntegrationView(_BrandsBaseView):
image: str,
) -> web.Response:
"""Handle GET request for an integration brand image."""
self._authenticate(request)
if not valid_domain(domain) or image not in ALLOWED_IMAGES:
return web.Response(status=HTTPStatus.NOT_FOUND)
@@ -274,8 +268,6 @@ class BrandsHardwareView(_BrandsBaseView):
image: str,
) -> web.Response:
"""Handle GET request for a hardware brand image."""
self._authenticate(request)
if not CATEGORY_RE.match(category):
return web.Response(status=HTTPStatus.NOT_FOUND)
# Hardware images have dynamic names like "manufacturer_model.png"
@@ -8,6 +8,6 @@
"iot_class": "local_push",
"loggers": ["aiostreammagic"],
"quality_scale": "platinum",
"requirements": ["aiostreammagic==2.13.1"],
"requirements": ["aiostreammagic==2.13.2"],
"zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."]
}
+14 -18
View File
@@ -2,7 +2,7 @@
import asyncio
import collections
from collections.abc import Awaitable, Callable, Coroutine
from collections.abc import Awaitable, Callable, Container, Coroutine, Mapping
from contextlib import suppress
from dataclasses import asdict, dataclass
from datetime import datetime, timedelta
@@ -12,16 +12,16 @@ import logging
import os
from random import SystemRandom
import time
from typing import Any, Final, final
from typing import Any, Final, final, override
from aiohttp import hdrs, web
from aiohttp import web
import attr
from propcache.api import cached_property, under_cached_property
import voluptuous as vol
from webrtc_models import RTCIceCandidateInit
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.media_player import (
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
@@ -776,30 +776,26 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
class CameraView(HomeAssistantView):
"""Base CameraView."""
requires_auth = False
use_query_token_for_auth = True
def __init__(self, component: EntityComponent[Camera]) -> None:
"""Initialize a basic camera view."""
self.component = component
@callback
@override
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
"""Return valid auth tokens, which can be used for query token authentication."""
if (camera := self.component.get_entity(match_info["entity_id"])) is None:
return ()
return camera.access_tokens
async def get(self, request: web.Request, entity_id: str) -> web.StreamResponse:
"""Start a GET request."""
if (camera := self.component.get_entity(entity_id)) is None:
raise web.HTTPNotFound
authenticated = (
request[KEY_AUTHENTICATED]
or request.query.get("token") in camera.access_tokens
)
if not authenticated:
# Attempt with invalid bearer token, raise unauthorized
# so ban middleware can handle it.
if hdrs.AUTHORIZATION in request.headers:
raise web.HTTPUnauthorized
# Invalid sigAuth or camera access token
raise web.HTTPForbidden
if not camera.is_on:
_LOGGER.debug("Camera is off")
raise web.HTTPServiceUnavailable
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.6.1"]
"requirements": ["hassil==3.6.0", "home-assistant-intents==2026.6.1"]
}
@@ -3,6 +3,8 @@
from collections.abc import Awaitable, Callable
from typing import Any
from hassil.parse_expression import parse_sentence
from hassil.parser import ParseError
from hassil.recognize import RecognizeResult
from hassil.util import (
PUNCTUATION_END,
@@ -42,6 +44,17 @@ def has_no_punctuation(value: list[str]) -> list[str]:
return value
def is_valid_sentence(value: list[str]) -> list[str]:
"""Validate result can be parsed by hassil."""
for sentence in value:
try:
parse_sentence(sentence)
except ParseError as err:
raise vol.Invalid(f"invalid sentence: {err}") from err
return value
def has_one_non_empty_item(value: list[str]) -> list[str]:
"""Validate result has at least one item."""
if len(value) < 1:
@@ -58,7 +71,11 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_PLATFORM): DOMAIN,
vol.Required(CONF_COMMAND): vol.All(
cv.ensure_list, [cv.string], has_one_non_empty_item, has_no_punctuation
cv.ensure_list,
[cv.string],
has_one_non_empty_item,
has_no_punctuation,
is_valid_sentence,
),
}
)
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pydaikin"],
"requirements": ["pydaikin==2.17.2"],
"requirements": ["pydaikin==2.18.1"],
"zeroconf": ["_dkapi._tcp.local."]
}
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["data-grand-lyon-ha==0.7.0"]
"requirements": ["data-grand-lyon-ha==0.8.0"]
}
+20 -2
View File
@@ -5,6 +5,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -27,7 +28,19 @@ async def async_setup_entry(
BinarySensorDeviceClass.MOISTURE,
),
DemoBinarySensor(
"binary_2", "Movement Backyard", True, BinarySensorDeviceClass.MOTION
"binary_2",
"Movement Backyard",
True,
BinarySensorDeviceClass.MOTION,
),
DemoBinarySensor(
"binary_3",
"Outside Temperature",
False,
BinarySensorDeviceClass.BATTERY_CHARGING,
device_id="sensor_1",
entity_category=EntityCategory.DIAGNOSTIC,
entity_name="Battery Charging",
),
]
)
@@ -46,6 +59,9 @@ class DemoBinarySensor(BinarySensorEntity):
device_name: str,
state: bool,
device_class: BinarySensorDeviceClass,
device_id: str | None = None,
entity_category: EntityCategory | None = None,
entity_name: str | None = None,
) -> None:
"""Initialize the demo sensor."""
self._unique_id = unique_id
@@ -54,10 +70,12 @@ class DemoBinarySensor(BinarySensorEntity):
self._attr_device_info = DeviceInfo(
identifiers={
# Serial numbers are unique identifiers within a specific domain
(DOMAIN, self.unique_id)
(DOMAIN, device_id or unique_id)
},
name=device_name,
)
self._attr_entity_category = entity_category
self._attr_name = entity_name
@property
def unique_id(self) -> str:
@@ -22,6 +22,7 @@ from .const import ( # noqa: F401
ATTR_LOCATION_NAME,
ATTR_MAC,
ATTR_SOURCE_TYPE,
ATTR_TRACKING_TYPE,
CONF_ASSOCIATED_ZONE,
CONF_CONSIDER_HOME,
CONF_NEW_DEVICE_DEFAULTS,
@@ -36,6 +37,7 @@ from .const import ( # noqa: F401
PLATFORM_TYPE_LEGACY,
SCAN_INTERVAL,
SourceType,
TrackingType,
)
from .entity import ( # noqa: F401
BaseScannerEntity,
@@ -25,6 +25,18 @@ class SourceType(StrEnum):
BLUETOOTH_LE = "bluetooth_le"
class TrackingType(StrEnum):
"""Tracking type for device trackers.
Describes how the tracker determines presence: by the device's geographic
position (e.g. GPS) or by its connection to a known endpoint (e.g. a router
or beacon associated with a zone).
"""
CONNECTION = "connection"
POSITION = "position"
CONF_SCAN_INTERVAL: Final = "interval_seconds"
SCAN_INTERVAL: Final = timedelta(seconds=12)
@@ -47,6 +59,7 @@ ATTR_IN_ZONES: Final = "in_zones"
ATTR_LOCATION_NAME: Final = "location_name"
ATTR_MAC: Final = "mac"
ATTR_SOURCE_TYPE: Final = "source_type"
ATTR_TRACKING_TYPE: Final = "tracking_type"
ATTR_CONSIDER_HOME: Final = "consider_home"
ATTR_IP: Final = "ip"
@@ -48,11 +48,13 @@ from .const import (
ATTR_IP,
ATTR_MAC,
ATTR_SOURCE_TYPE,
ATTR_TRACKING_TYPE,
CONF_ASSOCIATED_ZONE,
CONNECTED_DEVICE_REGISTERED,
DOMAIN,
LOGGER,
SourceType,
TrackingType,
)
_LOGGER = logging.getLogger(__name__)
@@ -238,6 +240,9 @@ class TrackerEntity(
"""Base class for a tracked device."""
entity_description: TrackerEntityDescription
_attr_capability_attributes: dict[str, Any] = {
ATTR_TRACKING_TYPE: TrackingType.POSITION
}
_attr_in_zones: list[str] | None = None
_attr_latitude: float | None = None
_attr_location_accuracy: float = 0
@@ -411,6 +416,9 @@ class BaseScannerEntity(BaseTrackerEntity):
addresses being used to identify the device.
"""
_attr_capability_attributes: dict[str, Any] = {
ATTR_TRACKING_TYPE: TrackingType.CONNECTION
}
_scanner_option_associated_zone: str = zone.ENTITY_ID_HOME
_scanner_option_associated_zone_unsub: CALLBACK_TYPE | None = None
@@ -40,6 +40,13 @@
"gps": "GPS",
"router": "Router"
}
},
"tracking_type": {
"name": "Tracking type",
"state": {
"connection": "Connection",
"position": "Position"
}
}
}
}
+2 -2
View File
@@ -16,7 +16,7 @@
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.2.7",
"aiodiscover==3.2.4",
"cached-ipaddress==1.1.1"
"aiodiscover==3.3.1",
"cached-ipaddress==1.1.2"
]
}
+3 -49
View File
@@ -13,7 +13,6 @@ from dsmr_parser.clients.rfxtrx_protocol import (
from dsmr_parser.objects import DSMRObject
import voluptuous as vol
from homeassistant.components import usb
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
@@ -23,6 +22,7 @@ from homeassistant.config_entries import (
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL, CONF_TYPE
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.selector import SerialPortSelector
from .const import (
CONF_DSMR_VERSION,
@@ -37,8 +37,6 @@ from .const import (
RFXTRX_DSMR_PROTOCOL,
)
CONF_MANUAL_PATH = "Enter Manually"
class DSMRConnection:
"""Test the connection to DSMR and receive telegram to read serial ids."""
@@ -165,8 +163,6 @@ class DSMRFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
_dsmr_version: str | None = None
@staticmethod
@callback
def async_get_options_flow(
@@ -222,34 +218,13 @@ class DSMRFlowHandler(ConfigFlow, domain=DOMAIN):
"""Step when setting up serial configuration."""
errors: dict[str, str] = {}
if user_input is not None:
user_selection = user_input[CONF_PORT]
if user_selection == CONF_MANUAL_PATH:
self._dsmr_version = user_input[CONF_DSMR_VERSION]
return await self.async_step_setup_serial_manual_path()
dev_path = user_selection
validate_data = {
CONF_PORT: dev_path,
CONF_DSMR_VERSION: user_input[CONF_DSMR_VERSION],
}
data = await self.async_validate_dsmr(validate_data, errors)
data = await self.async_validate_dsmr(user_input, errors)
if not errors:
return self.async_create_entry(title=data[CONF_PORT], data=data)
ports = await usb.async_scan_serial_ports(self.hass)
list_of_ports = {
port.device: f"{port.device} - {port.description or 'n/a'}"
f", s/n: {port.serial_number or 'n/a'}"
+ (f" - {port.manufacturer}" if port.manufacturer else "")
for port in ports
}
list_of_ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH
schema = vol.Schema(
{
vol.Required(CONF_PORT): vol.In(list_of_ports),
vol.Required(CONF_PORT): SerialPortSelector(),
vol.Required(CONF_DSMR_VERSION): vol.In(DSMR_VERSIONS),
}
)
@@ -259,27 +234,6 @@ class DSMRFlowHandler(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_setup_serial_manual_path(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Select path manually."""
if user_input is not None:
validate_data = {
CONF_PORT: user_input[CONF_PORT],
CONF_DSMR_VERSION: self._dsmr_version,
}
errors: dict[str, str] = {}
data = await self.async_validate_dsmr(validate_data, errors)
if not errors:
return self.async_create_entry(title=data[CONF_PORT], data=data)
schema = vol.Schema({vol.Required(CONF_PORT): str})
return self.async_show_form(
step_id="setup_serial_manual_path",
data_schema=schema,
)
async def async_validate_dsmr(
self, input_data: dict[str, Any], errors: dict[str, str]
) -> dict[str, Any]:
@@ -26,12 +26,6 @@
},
"title": "[%key:common::config_flow::data::device%]"
},
"setup_serial_manual_path": {
"data": {
"port": "[%key:common::config_flow::data::usb_path%]"
},
"title": "[%key:common::config_flow::data::path%]"
},
"user": {
"data": {
"type": "Connection type"
+1
View File
@@ -7,3 +7,4 @@ from homeassistant.const import Platform
DOMAIN = "duco"
PLATFORMS = [Platform.FAN, Platform.SENSOR]
SCAN_INTERVAL = timedelta(seconds=10)
BOX_NODE_ID = 1
+7 -1
View File
@@ -3,7 +3,7 @@
from dataclasses import asdict
from typing import Any
from duco_connectivity.exceptions import DucoConnectionError
from duco_connectivity.exceptions import DucoConnectionError, DucoError
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_HOST
@@ -52,6 +52,12 @@ async def async_get_config_entry_diagnostics(
translation_domain=DOMAIN,
translation_key="connection_error",
) from err
except DucoError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={"error": repr(err)},
) from err
api_info: dict[str, Any] = {"public_api_version": api_info_obj.public_api_version}
if api_info_obj.reported_api_version is not None:
+3 -3
View File
@@ -1,6 +1,6 @@
"""Base entity for the Duco integration."""
from duco_connectivity.models import Node
from duco_connectivity.models import Node, NodeType
from homeassistant.const import ATTR_VIA_DEVICE
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
@@ -25,7 +25,7 @@ class DucoEntity(CoordinatorEntity[DucoCoordinator]):
identifiers={(DOMAIN, f"{mac}_{node.node_id}")},
manufacturer="Duco",
model=coordinator.board_info.box_name
if node.general.node_type == "BOX"
if node.general.node_type == NodeType.BOX
else node.general.node_type,
name=node.general.name or f"Node {node.node_id}",
)
@@ -34,7 +34,7 @@ class DucoEntity(CoordinatorEntity[DucoCoordinator]):
"connections": {(CONNECTION_NETWORK_MAC, mac)},
"serial_number": coordinator.board_info.serial_board_box,
}
if node.general.node_type == "BOX"
if node.general.node_type == NodeType.BOX
else {ATTR_VIA_DEVICE: (DOMAIN, f"{mac}_1")}
)
self._attr_device_info = device_info
+1 -1
View File
@@ -13,7 +13,7 @@
"iot_class": "local_polling",
"loggers": ["duco_connectivity"],
"quality_scale": "platinum",
"requirements": ["python-duco-connectivity==0.5.0"],
"requirements": ["python-duco-connectivity==0.6.0"],
"zeroconf": [
{
"name": "duco [[][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][]].*",
+8 -2
View File
@@ -24,7 +24,7 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .const import BOX_NODE_ID, DOMAIN
from .coordinator import DucoConfigEntry, DucoCoordinator
from .entity import DucoEntity
@@ -158,7 +158,13 @@ async def async_setup_entry(
# The firmware removes deregistered RF/wired nodes automatically.
# BSRH box sensors that are physically unplugged from the PCB are
# not deregistered by the firmware and will never appear here as stale.
stale_node_ids = known_nodes - coordinator.data.nodes.keys()
# The BOX node can transiently disappear from the API response, so keep
# node 1 to avoid removing the main controller device.
stale_node_ids = {
node_id
for node_id in known_nodes - coordinator.data.nodes.keys()
if node_id != BOX_NODE_ID
}
if stale_node_ids:
device_reg = dr.async_get(hass)
mac = entry.unique_id
+1 -1
View File
@@ -59,7 +59,7 @@
"name": "Target flow level"
},
"time_state_end": {
"name": "Mode end time"
"name": "State end time"
},
"ventilation_state": {
"name": "Ventilation state",
@@ -9,7 +9,6 @@ Warnungen vor markantem Wetter (Stufe 2) # codespell:ignore vor
Wetterwarnungen (Stufe 1)
"""
from datetime import UTC, datetime
from typing import Any
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
@@ -17,6 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from .const import (
ADVANCE_WARNING_SENSOR,
@@ -100,7 +100,7 @@ class DwdWeatherWarningsSensor(
if warnings is None:
return []
now = datetime.now(UTC) # pylint: disable=home-assistant-enforce-utcnow
now = dt_util.utcnow()
return [warning for warning in warnings if warning[API_ATTR_WARNING_END] > now]
@property
@@ -0,0 +1,18 @@
"""Edifier infrared integration for Home Assistant."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
PLATFORMS = [Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Edifier IR from a config entry."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload an Edifier IR config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -0,0 +1,77 @@
"""Config flow for Edifier infrared integration."""
from typing import Any
from infrared_protocols.codes.edifier.models import MODEL_TO_COMMAND_SET, EdifierModel
import voluptuous as vol
from homeassistant.components.infrared import (
DOMAIN as INFRARED_DOMAIN,
async_get_emitters,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_MODEL
from homeassistant.helpers.selector import (
EntitySelector,
EntitySelectorConfig,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
from .const import CONF_COMMAND_SET, CONF_INFRARED_ENTITY_ID, DOMAIN
class EdifierIrConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle config flow for Edifier IR."""
VERSION = 1
MINOR_VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step - select IR entity and speaker model."""
emitter_entity_ids = async_get_emitters(self.hass)
if not emitter_entity_ids:
return self.async_abort(reason="no_emitters")
if user_input is not None:
infrared_entity_id = user_input[CONF_INFRARED_ENTITY_ID]
model = EdifierModel(user_input[CONF_MODEL])
command_set = MODEL_TO_COMMAND_SET[model]
await self.async_set_unique_id(f"{command_set}_{infrared_entity_id}")
self._abort_if_unique_id_configured()
entity_name = infrared_entity_id
if state := self.hass.states.get(infrared_entity_id):
entity_name = state.name or infrared_entity_id
return self.async_create_entry(
title=f"Edifier {model.value} via {entity_name}",
data={
CONF_INFRARED_ENTITY_ID: infrared_entity_id,
CONF_MODEL: model.value,
CONF_COMMAND_SET: command_set.value,
},
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_INFRARED_ENTITY_ID): EntitySelector(
EntitySelectorConfig(
domain=INFRARED_DOMAIN, include_entities=emitter_entity_ids
)
),
vol.Required(CONF_MODEL): SelectSelector(
SelectSelectorConfig(
options=[model.value for model in EdifierModel],
mode=SelectSelectorMode.DROPDOWN,
)
),
}
),
)
@@ -0,0 +1,19 @@
"""Constants for the Edifier infrared integration."""
from infrared_protocols.codes.edifier.r1280db import EdifierR1280DBCode
from infrared_protocols.codes.edifier.r1280t import EdifierR1280TCode
from infrared_protocols.codes.edifier.r1700bt import EdifierR1700BTCode
from infrared_protocols.codes.edifier.rc20g import EdifierRC20GCode
from infrared_protocols.codes.edifier.s360db import EdifierS360DBCode
DOMAIN = "edifier_infrared"
CONF_INFRARED_ENTITY_ID = "infrared_entity_id"
CONF_COMMAND_SET = "command_set"
type EdifierCode = (
EdifierR1700BTCode
| EdifierR1280DBCode
| EdifierR1280TCode
| EdifierS360DBCode
| EdifierRC20GCode
)
@@ -0,0 +1,27 @@
"""Common entity for Edifier infrared integration."""
from infrared_protocols.codes.edifier.models import EdifierModel
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from .const import DOMAIN
class EdifierIrEntity(Entity):
"""Edifier IR base entity providing common device info."""
_attr_has_entity_name = True
def __init__(
self, entry: ConfigEntry, model: EdifierModel, unique_id_suffix: str
) -> None:
"""Initialize Edifier IR entity."""
self._attr_unique_id = f"{entry.entry_id}_{unique_id_suffix}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
name=f"Edifier {model.value}",
manufacturer="Edifier",
model=model.value,
)
@@ -0,0 +1,11 @@
{
"domain": "edifier_infrared",
"name": "Edifier Infrared",
"codeowners": ["@abmantis"],
"config_flow": true,
"dependencies": ["infrared"],
"documentation": "https://www.home-assistant.io/integrations/edifier_infrared",
"integration_type": "device",
"iot_class": "assumed_state",
"quality_scale": "bronze"
}
@@ -0,0 +1,174 @@
"""Media player platform for Edifier infrared integration."""
from infrared_protocols.codes.edifier.models import EdifierCommandSet, EdifierModel
from infrared_protocols.codes.edifier.r1280db import EdifierR1280DBCode
from infrared_protocols.codes.edifier.r1280t import EdifierR1280TCode
from infrared_protocols.codes.edifier.r1700bt import EdifierR1700BTCode
from infrared_protocols.codes.edifier.rc20g import EdifierRC20GCode
from infrared_protocols.codes.edifier.s360db import EdifierS360DBCode
from homeassistant.components.infrared import InfraredEmitterConsumerEntity
from homeassistant.components.media_player import (
MediaPlayerDeviceClass,
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MODEL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import CONF_COMMAND_SET, CONF_INFRARED_ENTITY_ID, EdifierCode
from .entity import EdifierIrEntity
PARALLEL_UPDATES = 1
COMMAND_SET_COMMANDS: dict[
EdifierCommandSet,
dict[
MediaPlayerEntityFeature,
tuple[EdifierCode | tuple[EdifierCode, ...], ...],
],
] = {
EdifierCommandSet.R1700BT: {
MediaPlayerEntityFeature.TURN_ON: (EdifierR1700BTCode.POWER,),
MediaPlayerEntityFeature.TURN_OFF: (EdifierR1700BTCode.POWER,),
MediaPlayerEntityFeature.VOLUME_STEP: (
(EdifierR1700BTCode.VOLUME_UP,),
(EdifierR1700BTCode.VOLUME_DOWN,),
),
MediaPlayerEntityFeature.VOLUME_MUTE: (EdifierR1700BTCode.MUTE,),
MediaPlayerEntityFeature.PLAY: (EdifierR1700BTCode.PLAY_PAUSE,),
MediaPlayerEntityFeature.PAUSE: (EdifierR1700BTCode.PLAY_PAUSE,),
MediaPlayerEntityFeature.NEXT_TRACK: (EdifierR1700BTCode.FORWARD,),
MediaPlayerEntityFeature.PREVIOUS_TRACK: (EdifierR1700BTCode.BACK,),
},
EdifierCommandSet.R1280DB: {
MediaPlayerEntityFeature.TURN_ON: (EdifierR1280DBCode.POWER,),
MediaPlayerEntityFeature.TURN_OFF: (EdifierR1280DBCode.POWER,),
MediaPlayerEntityFeature.VOLUME_STEP: (
(EdifierR1280DBCode.VOLUME_UP,),
(EdifierR1280DBCode.VOLUME_DOWN,),
),
MediaPlayerEntityFeature.VOLUME_MUTE: (EdifierR1280DBCode.MUTE,),
MediaPlayerEntityFeature.PLAY: (EdifierR1280DBCode.PLAY_PAUSE,),
MediaPlayerEntityFeature.PAUSE: (EdifierR1280DBCode.PLAY_PAUSE,),
MediaPlayerEntityFeature.NEXT_TRACK: (EdifierR1280DBCode.FORWARD,),
MediaPlayerEntityFeature.PREVIOUS_TRACK: (EdifierR1280DBCode.BACK,),
},
EdifierCommandSet.R1280T: {
MediaPlayerEntityFeature.VOLUME_STEP: (
(EdifierR1280TCode.VOLUME_UP,),
(EdifierR1280TCode.VOLUME_DOWN,),
),
MediaPlayerEntityFeature.VOLUME_MUTE: (EdifierR1280TCode.MUTE,),
},
EdifierCommandSet.S360DB: {
MediaPlayerEntityFeature.TURN_ON: (EdifierS360DBCode.POWER,),
MediaPlayerEntityFeature.TURN_OFF: (EdifierS360DBCode.POWER,),
MediaPlayerEntityFeature.VOLUME_STEP: (
(EdifierS360DBCode.VOLUME_UP,),
(EdifierS360DBCode.VOLUME_DOWN,),
),
MediaPlayerEntityFeature.PLAY: (EdifierS360DBCode.PLAY_PAUSE,),
MediaPlayerEntityFeature.PAUSE: (EdifierS360DBCode.PLAY_PAUSE,),
MediaPlayerEntityFeature.NEXT_TRACK: (EdifierS360DBCode.NEXT,),
MediaPlayerEntityFeature.PREVIOUS_TRACK: (EdifierS360DBCode.PREVIOUS,),
},
EdifierCommandSet.RC20G: {
MediaPlayerEntityFeature.TURN_ON: (EdifierRC20GCode.POWER,),
MediaPlayerEntityFeature.TURN_OFF: (EdifierRC20GCode.POWER,),
MediaPlayerEntityFeature.VOLUME_STEP: (
(EdifierRC20GCode.VOLUME_UP_LEFT, EdifierRC20GCode.VOLUME_UP_RIGHT),
(EdifierRC20GCode.VOLUME_DOWN_LEFT, EdifierRC20GCode.VOLUME_DOWN_RIGHT),
),
MediaPlayerEntityFeature.VOLUME_MUTE: (EdifierRC20GCode.MUTE,),
MediaPlayerEntityFeature.PLAY: (EdifierRC20GCode.PLAY_PAUSE,),
MediaPlayerEntityFeature.PAUSE: (EdifierRC20GCode.PLAY_PAUSE,),
MediaPlayerEntityFeature.NEXT_TRACK: (EdifierRC20GCode.FORWARD,),
MediaPlayerEntityFeature.PREVIOUS_TRACK: (EdifierRC20GCode.PREVIOUS,),
},
}
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Edifier IR media player."""
infrared_entity_id = entry.data[CONF_INFRARED_ENTITY_ID]
command_set = EdifierCommandSet(entry.data[CONF_COMMAND_SET])
model = EdifierModel(entry.data[CONF_MODEL])
async_add_entities(
[EdifierIrMediaPlayer(entry, model, infrared_entity_id, command_set)]
)
class EdifierIrMediaPlayer(
EdifierIrEntity, InfraredEmitterConsumerEntity, MediaPlayerEntity
):
"""Edifier IR media player entity."""
_attr_name = None
_attr_assumed_state = True
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
def __init__(
self,
entry: ConfigEntry,
model: EdifierModel,
infrared_entity_id: str,
command_set: EdifierCommandSet,
) -> None:
"""Initialize Edifier IR media player."""
super().__init__(entry, model, unique_id_suffix="media_player")
self._infrared_emitter_entity_id = infrared_entity_id
self._commands = COMMAND_SET_COMMANDS[command_set]
self._attr_state = MediaPlayerState.ON
self._attr_supported_features = MediaPlayerEntityFeature(0)
for feature in self._commands:
self._attr_supported_features |= feature
async def _send_codes(self, *codes: EdifierCode) -> None:
"""Send one or more IR commands."""
for code in codes:
await self._send_command(code.to_command())
async def async_turn_on(self) -> None:
"""Turn on the speaker."""
await self._send_codes(*self._commands[MediaPlayerEntityFeature.TURN_ON])
async def async_turn_off(self) -> None:
"""Turn off the speaker."""
await self._send_codes(*self._commands[MediaPlayerEntityFeature.TURN_OFF])
async def async_volume_up(self) -> None:
"""Send volume up command."""
await self._send_codes(*self._commands[MediaPlayerEntityFeature.VOLUME_STEP][0])
async def async_volume_down(self) -> None:
"""Send volume down command."""
await self._send_codes(*self._commands[MediaPlayerEntityFeature.VOLUME_STEP][1])
async def async_mute_volume(self, mute: bool) -> None:
"""Send mute command."""
await self._send_codes(*self._commands[MediaPlayerEntityFeature.VOLUME_MUTE])
async def async_media_play(self) -> None:
"""Send play command."""
await self._send_codes(*self._commands[MediaPlayerEntityFeature.PLAY])
async def async_media_pause(self) -> None:
"""Send pause command."""
await self._send_codes(*self._commands[MediaPlayerEntityFeature.PAUSE])
async def async_media_next_track(self) -> None:
"""Send next track command."""
await self._send_codes(*self._commands[MediaPlayerEntityFeature.NEXT_TRACK])
async def async_media_previous_track(self) -> None:
"""Send previous track command."""
await self._send_codes(*self._commands[MediaPlayerEntityFeature.PREVIOUS_TRACK])
@@ -0,0 +1,114 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration does not provide additional actions.
appropriate-polling:
status: exempt
comment: |
This integration does not poll.
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
This integration does not provide additional actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data:
status: exempt
comment: |
This integration does not store runtime data.
test-before-configure: done
test-before-setup:
status: exempt
comment: |
This integration only proxies commands through an existing infrared
entity, so there is no separate connection to validate during setup.
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
This integration does not register custom actions.
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow:
status: exempt
comment: |
This integration does not require authentication.
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: |
This integration does not support discovery.
discovery:
status: exempt
comment: |
Discovery is not supported for infrared integrations.
docs-data-update:
status: exempt
comment: |
This integration does not fetch data from devices.
docs-examples: todo
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: done
dynamic-devices:
status: exempt
comment: |
Each config entry creates a single device.
entity-category:
status: exempt
comment: |
The media player entity is the primary entity and does not need a category.
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: |
The media player entity is the primary entity and should be enabled by default.
entity-translations: done
exception-translations:
status: exempt
comment: |
This integration does not raise exceptions.
icon-translations: done
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: |
This integration does not have repairable issues.
stale-devices:
status: exempt
comment: |
Each config entry manages exactly one device.
# Platinum
async-dependency:
status: exempt
comment: |
This integration depends on infrared_protocols which provides only code
definitions with no I/O, so async dependency does not apply.
inject-websession:
status: exempt
comment: |
This integration does not make HTTP requests.
strict-typing: todo
@@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "This Edifier device has already been configured with this transmitter.",
"no_emitters": "No infrared transmitter entities found. Please set up an infrared device first."
},
"step": {
"user": {
"data": {
"infrared_entity_id": "IR transmitter",
"model": "Speaker model"
},
"data_description": {
"infrared_entity_id": "Select the infrared transmitter entity to use.",
"model": "Choose your Edifier speaker model from the list."
},
"description": "Configure your Edifier speaker for IR control.",
"title": "Set up Edifier IR speaker"
}
}
}
}
@@ -26,6 +26,7 @@ from homeassistant.helpers.event import (
async_track_state_change_event,
async_track_time_interval,
)
from homeassistant.util import dt as dt_util
from .const import (
CONF_DEVICE_NAME,
@@ -221,8 +222,7 @@ def update_listeners(hass: HomeAssistant, entry: EnergyIDConfigEntry) -> None:
):
try:
value = float(current_state.state)
# pylint: disable-next=home-assistant-enforce-utcnow
timestamp = current_state.last_updated or dt.datetime.now(dt.UTC)
timestamp = current_state.last_updated or dt_util.utcnow()
client.get_or_create_sensor(energyid_key).update(value, timestamp)
except ValueError, TypeError:
_LOGGER.debug(
@@ -166,6 +166,8 @@ class RuntimeEntryData:
)
loaded_platforms: set[Platform] = field(default_factory=set)
platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
# Set once the first connection has finished scanner setup or teardown.
first_connect_done: asyncio.Event = field(default_factory=asyncio.Event)
_storage_contents: StoreData | None = None
_pending_storage: Callable[[], StoreData] | None = None
assist_pipeline_update_callbacks: list[CALLBACK_TYPE] = field(default_factory=list)
+22 -1
View File
@@ -1,11 +1,12 @@
"""Manager for esphome devices."""
import asyncio
import base64
from functools import partial
import logging
import secrets
import struct
from typing import TYPE_CHECKING, Any, NamedTuple
from typing import TYPE_CHECKING, Any, Final, NamedTuple
from aioesphomeapi import (
APIClient,
@@ -106,6 +107,9 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
# Max time to wait at startup for a BLE proxy to register its scanner.
STARTUP_SCANNER_WAIT: Final = 3.0
LOG_LEVEL_TO_LOGGER = {
LogLevel.LOG_LEVEL_NONE: logging.DEBUG,
LogLevel.LOG_LEVEL_ERROR: logging.ERROR,
@@ -677,6 +681,8 @@ class ESPHomeManager:
hass, device_info.bluetooth_mac_address or device_info.mac_address
)
entry_data.first_connect_done.set()
if device_info.voice_assistant_feature_flags_compat(api_version) and (
Platform.ASSIST_SATELLITE not in entry_data.loaded_platforms
):
@@ -988,6 +994,21 @@ class ESPHomeManager:
await reconnect_logic.start()
# Wait for a cached BLE proxy to register its scanner before finishing setup.
if (
device_info := entry_data.device_info
) is not None and device_info.bluetooth_proxy_feature_flags_compat(
entry_data.api_version
):
try:
async with asyncio.timeout(STARTUP_SCANNER_WAIT):
await entry_data.first_connect_done.wait()
except TimeoutError:
_LOGGER.debug(
"%s: Timed out waiting for Bluetooth scanner to register",
self.host,
)
@callback
def _async_setup_device_registry(
@@ -24,6 +24,8 @@ ATTR_DURATION: Final = "duration" # number of minutes, <24h
ATTR_PERIOD: Final = "period" # number of days
ATTR_SETPOINT: Final = "setpoint"
# Support for the refresh_system service is being deprecated
REFRESH_BREAKS_IN_HA_VERSION: Final = "2027.1.0"
# Support for the reset service calls/presets is being deprecated
RESET_BREAKS_IN_HA_VERSION: Final = "2026.11.0"
# Support for untargeted service calls to controllers is being deprecated
@@ -29,6 +29,7 @@ from .const import (
ATTR_PERIOD,
ATTR_SETPOINT,
DOMAIN,
REFRESH_BREAKS_IN_HA_VERSION,
RESET_BREAKS_IN_HA_VERSION,
SERVICE_BREAKS_IN_HA_VERSION,
EvoService,
@@ -204,6 +205,11 @@ def setup_service_functions(
@verify_domain_control(DOMAIN)
async def force_refresh(call: ServiceCall) -> None:
"""Obtain the latest state data via the vendor's RESTful API."""
async_create_deprecation_issue_once(
hass,
"deprecated_refresh_system_service",
REFRESH_BREAKS_IN_HA_VERSION,
)
await coordinator.async_refresh()
@verify_domain_control(DOMAIN)
@@ -31,13 +31,17 @@
"title": "Evohome 'Clear zone override' action is deprecated"
},
"deprecated_controller_service": {
"description": "The `{service}` action without `entity_id` is deprecated and will stop working in Home Assistant {breaks_in_ha_version}. Update any automation or script to include the Evohome controller climate entity `entity_id`.",
"description": "The `{service}` action without `entity_id` is deprecated and will stop working in Home Assistant {breaks_in_ha_version}. Update any automation or script to include `entity_id`, targeting the controller's climate entity.",
"title": "Untargeted Evohome controller action is deprecated"
},
"deprecated_preset_reset": {
"description": "Using the `Reset` preset on an Evohome controller is deprecated and will stop working in Home Assistant {breaks_in_ha_version}. Use the system's Reset button instead.",
"title": "Evohome Reset preset is deprecated"
},
"deprecated_refresh_system_service": {
"description": "The `refresh_system` action is deprecated and will stop working in Home Assistant {breaks_in_ha_version}. Instead, use the `homeassistant.update_entity` action, targeting the controller's climate entity.",
"title": "Evohome 'Refresh system' action is deprecated"
},
"deprecated_reset_system_service": {
"description": "The `reset_system` action is deprecated and will stop working in Home Assistant {breaks_in_ha_version}. Use the system's Reset button instead.",
"title": "Evohome 'Reset system' action is deprecated"
@@ -49,7 +53,7 @@
"name": "Clear zone override"
},
"refresh_system": {
"description": "Pulls the latest data from the vendor's servers now, rather than waiting for the next scheduled update.",
"description": "Pulls the latest data from the vendor's servers now, rather than waiting for the next scheduled update (deprecated).",
"name": "Refresh system"
},
"reset_system": {
@@ -7,6 +7,7 @@ from fluss_api import (
FlussApiClient,
FlussApiClientAuthenticationError,
FlussApiClientError,
FlussDeviceOfflineError,
)
from homeassistant.config_entries import ConfigEntry
@@ -45,11 +46,13 @@ class FlussDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]
)
async def _async_get_status(self, device_id: str) -> dict[str, Any]:
"""Return per-device status."""
"""Return per-device status, treating an offline device as disconnected."""
try:
response = await self.api.async_get_device_status(device_id)
except FlussDeviceOfflineError:
return {"internetConnected": False}
except FlussApiClientError as err:
raise UpdateFailed(f"Error fetching status for {device_id}: {err}") from err
raise UpdateFailed(f"Error fetching Fluss device status: {err}") from err
return response["status"]
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
+1 -1
View File
@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["fluss-api"],
"quality_scale": "bronze",
"requirements": ["fluss-api==0.2.4"]
"requirements": ["fluss-api==0.2.5"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/forecast_solar",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["forecast-solar==5.0.0"]
"requirements": ["forecast-solar==5.0.1"]
}
@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260527.3"]
"requirements": ["home-assistant-frontend==20260527.5"]
}
@@ -1,17 +1,20 @@
"""The Gardena Bluetooth integration."""
from contextlib import suppress
import logging
from bleak.backends.device import BLEDevice
from gardena_bluetooth.client import CachedConnection, Client
from gardena_bluetooth.const import ProductType
from gardena_bluetooth.scan import async_get_manufacturer_data
from gardena_bluetooth.const import ScanService
from gardena_bluetooth.parse import ManufacturerData, ProductType
from habluetooth import BluetoothServiceInfoBleak
from homeassistant.components import bluetooth
from homeassistant.const import CONF_ADDRESS, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import CONF_PRODUCT_TYPE
from .coordinator import (
DeviceUnavailable,
GardenaBluetoothConfigEntry,
@@ -30,6 +33,79 @@ PLATFORMS: list[Platform] = [
]
LOGGER = logging.getLogger(__name__)
DISCONNECT_DELAY = 5
PRODUCTS_SCAN_TIMEOUT = 10
PRODUCT_TYPE_TIMEOUT = 30
async def async_get_product(hass: HomeAssistant, address: str) -> ManufacturerData:
"""Get manufacturer data for the given address via active scan."""
data = ManufacturerData()
def _data_callback(info: BluetoothServiceInfoBleak) -> bool:
LOGGER.debug("Processing advertisement from %s: %s", info.address, info)
if info.device.address != address:
return False
data.update(info.manufacturer_data.get(ManufacturerData.company, b""))
return data.product_type is not ProductType.UNKNOWN
with suppress(TimeoutError):
await bluetooth.async_process_advertisements(
hass,
_data_callback,
bluetooth.BluetoothCallbackMatcher(
address=address, manufacturer_id=ManufacturerData.company
),
mode=bluetooth.BluetoothScanningMode.ACTIVE,
timeout=PRODUCT_TYPE_TIMEOUT,
)
return data
async def async_get_products(hass: HomeAssistant) -> dict[str, ManufacturerData]:
"""Get all products that are currently advertising."""
products: dict[str, ManufacturerData] = {}
def _data_callback(info: BluetoothServiceInfoBleak) -> bool:
LOGGER.debug("Processing advertisement from %s: %s", info.address, info)
if ScanService not in info.service_uuids:
return False
raw = info.manufacturer_data.get(ManufacturerData.company, b"")
if (data := products.get(info.device.address)) is None:
data = ManufacturerData()
products[info.device.address] = data
data.update(raw)
return False
with suppress(TimeoutError):
await bluetooth.async_process_advertisements(
hass,
_data_callback,
bluetooth.BluetoothCallbackMatcher(
manufacturer_id=ManufacturerData.company
),
mode=bluetooth.BluetoothScanningMode.ACTIVE,
timeout=PRODUCTS_SCAN_TIMEOUT,
)
return products
async def async_migrate_product_type(
hass: HomeAssistant, entry: GardenaBluetoothConfigEntry
) -> GardenaBluetoothConfigEntry:
"""Discover product type for old entries and upgrade them to minor version 2."""
mfg = await async_get_product(hass, entry.data[CONF_ADDRESS])
if mfg.product_type is ProductType.UNKNOWN:
raise ConfigEntryNotReady("Unable to find product type")
hass.config_entries.async_update_entry(
entry,
data={**entry.data, CONF_PRODUCT_TYPE: mfg.product_type.name},
minor_version=2,
)
return entry
def get_connection(hass: HomeAssistant, address: str) -> CachedConnection:
@@ -51,16 +127,11 @@ async def async_setup_entry(
) -> bool:
"""Set up Gardena Bluetooth from a config entry."""
if entry.minor_version < 2:
entry = await async_migrate_product_type(hass, entry)
address = entry.data[CONF_ADDRESS]
try:
mfg_data = await async_get_manufacturer_data({address})
except TimeoutError as exc:
raise ConfigEntryNotReady("Unable to find product type") from exc
product_type = mfg_data[address].product_type
if product_type is ProductType.UNKNOWN:
raise ConfigEntryNotReady("Unable to find product type")
product_type = ProductType[entry.data[CONF_PRODUCT_TYPE]]
client = Client(get_connection(hass, address), product_type)
@@ -4,22 +4,18 @@ import logging
from typing import Any
from gardena_bluetooth.client import Client
from gardena_bluetooth.const import PRODUCT_NAMES, DeviceInformation, ScanService
from gardena_bluetooth.const import PRODUCT_NAMES, DeviceInformation
from gardena_bluetooth.exceptions import CharacteristicNotFound, CommunicationFailure
from gardena_bluetooth.parse import ManufacturerData, ProductType
from gardena_bluetooth.scan import async_get_manufacturer_data
import voluptuous as vol
from homeassistant.components.bluetooth import (
BluetoothServiceInfo,
async_discovered_service_info,
)
from homeassistant.components.bluetooth import BluetoothServiceInfo
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ADDRESS
from homeassistant.data_entry_flow import AbortFlow
from . import get_connection
from .const import DOMAIN
from . import async_get_product, async_get_products, get_connection
from .const import CONF_PRODUCT_TYPE, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -33,26 +29,16 @@ _SUPPORTED_PRODUCT_TYPES = {
}
def _is_supported(discovery_info: BluetoothServiceInfo):
"""Check if device is supported."""
if ScanService not in discovery_info.service_uuids:
return False
if discovery_info.manufacturer_data.get(ManufacturerData.company) is None:
_LOGGER.debug("Missing manufacturer data: %s", discovery_info)
return False
return True
class GardenaBluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Gardena Bluetooth."""
VERSION = 1
MINOR_VERSION = 2
def __init__(self) -> None:
"""Initialize the config flow."""
self.devices: dict[str, str] = {}
self.address: str | None
self.devices: dict[str, ManufacturerData] = {}
async def async_read_data(self):
"""Try to connect to device and extract information."""
@@ -68,20 +54,23 @@ class GardenaBluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
finally:
await client.disconnect()
return {CONF_ADDRESS: self.address}
assert self.address in self.devices
return {
CONF_ADDRESS: self.address,
CONF_PRODUCT_TYPE: self.devices[self.address].product_type.name,
}
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfo
) -> ConfigFlowResult:
"""Handle the bluetooth discovery step."""
_LOGGER.debug("Discovered device: %s", discovery_info)
data = await async_get_manufacturer_data({discovery_info.address})
product_type = data[discovery_info.address].product_type
if product_type not in _SUPPORTED_PRODUCT_TYPES:
mfg = await async_get_product(self.hass, discovery_info.address)
self.devices[discovery_info.address] = mfg
if mfg.product_type not in _SUPPORTED_PRODUCT_TYPES:
return self.async_abort(reason="no_devices_found")
self.address = discovery_info.address
self.devices = {discovery_info.address: PRODUCT_NAMES[product_type]}
await self.async_set_unique_id(self.address)
self._abort_if_unique_id_configured()
return await self.async_step_confirm()
@@ -91,7 +80,7 @@ class GardenaBluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Confirm discovery."""
assert self.address
title = self.devices[self.address]
title = PRODUCT_NAMES[self.devices[self.address].product_type]
if user_input is not None:
data = await self.async_read_data()
@@ -117,31 +106,25 @@ class GardenaBluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured()
return await self.async_step_confirm()
current_addresses = self._async_current_ids(include_ignore=False)
candidates = set()
for discovery_info in async_discovered_service_info(self.hass):
address = discovery_info.address
if address in current_addresses or not _is_supported(discovery_info):
continue
candidates.add(address)
data = await async_get_manufacturer_data(candidates)
for address, mfg_data in data.items():
if mfg_data.product_type not in _SUPPORTED_PRODUCT_TYPES:
continue
self.devices[address] = PRODUCT_NAMES[mfg_data.product_type]
current = self._async_current_ids(include_ignore=False)
self.devices = await async_get_products(self.hass)
# Keep selection sorted by address to ensure stable tests
self.devices = dict(sorted(self.devices.items(), key=lambda x: x[0]))
devices = {
address: PRODUCT_NAMES[data.product_type]
for address in sorted(self.devices)
if address not in current
and (data := self.devices[address]).product_type in _SUPPORTED_PRODUCT_TYPES
}
if not self.devices:
if not devices:
return self.async_abort(reason="no_devices_found")
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_ADDRESS): vol.In(self.devices),
vol.Required(CONF_ADDRESS): vol.In(devices),
},
),
)
@@ -1,3 +1,4 @@
"""Constants for the Gardena Bluetooth integration."""
DOMAIN = "gardena_bluetooth"
CONF_PRODUCT_TYPE = "product_type"
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/generic",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["av==16.0.1", "Pillow==12.2.0"]
"requirements": ["av==17.0.1", "Pillow==12.2.0"]
}
@@ -1,11 +1,12 @@
{
"domain": "gentex_homelink",
"name": "HomeLink",
"codeowners": ["@niaexa", "@ryanjones-gentex"],
"codeowners": ["@Gentex-Corporation/Homelink", "@rjones-gentex"],
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/gentex_homelink",
"integration_type": "hub",
"iot_class": "cloud_push",
"quality_scale": "bronze",
"requirements": ["homelink-integration-api==0.0.1"]
"requirements": ["homelink-integration-api==0.0.5"]
}
@@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__)
class GoodweFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a Goodwe config flow."""
MINOR_VERSION = 2
VERSION = 2
async def async_handle_successful_connection(
self,
@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/hassio",
"iot_class": "local_polling",
"quality_scale": "internal",
"requirements": ["aiohasupervisor==0.4.3"],
"requirements": ["aiohasupervisor==0.5.0"],
"single_config_entry": true
}
+1 -1
View File
@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
from .coordinator import HeltyConfigEntry, HeltyDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.FAN, Platform.SENSOR]
PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.FAN, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: HeltyConfigEntry) -> bool:
+47
View File
@@ -0,0 +1,47 @@
"""Button platform for the Helty Flow integration."""
from pyhelty import HeltyError
from homeassistant.components.button import ButtonEntity
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import HeltyConfigEntry, HeltyDataUpdateCoordinator
from .entity import HeltyEntity
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
entry: HeltyConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Helty buttons."""
async_add_entities([HeltyResetFilterButton(entry.runtime_data)])
class HeltyResetFilterButton(HeltyEntity, ButtonEntity):
"""Resets the filter-life counter after the filter has been replaced."""
_attr_entity_category = EntityCategory.CONFIG
_attr_translation_key = "reset_filter"
def __init__(self, coordinator: HeltyDataUpdateCoordinator) -> None:
"""Initialize the button."""
super().__init__(coordinator)
self._attr_unique_id = f"{self._device_id}_reset_filter"
async def async_press(self) -> None:
"""Reset the filter-life counter."""
try:
await self.coordinator.client.async_reset_filter()
except HeltyError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="reset_filter_failed",
) from err
await self.coordinator.async_request_refresh()
+10 -3
View File
@@ -2,17 +2,18 @@
from typing import Any
from pyhelty import FanMode
from pyhelty import FanMode, HeltyError
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.percentage import (
ordered_list_item_to_percentage,
percentage_to_ordered_list_item,
)
from .const import PRESET_BOOST, PRESET_FREE_COOLING, PRESET_NIGHT
from .const import DOMAIN, PRESET_BOOST, PRESET_FREE_COOLING, PRESET_NIGHT
from .coordinator import HeltyConfigEntry, HeltyDataUpdateCoordinator
from .entity import HeltyEntity
@@ -116,5 +117,11 @@ class HeltyFan(HeltyEntity, FanEntity):
await self._async_set_mode(FanMode.OFF)
async def _async_set_mode(self, mode: FanMode) -> None:
await self.coordinator.client.async_set_fan_mode(mode)
try:
await self.coordinator.client.async_set_fan_mode(mode)
except HeltyError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="set_fan_mode_failed",
) from err
await self.coordinator.async_request_refresh()
@@ -26,9 +26,7 @@ rules:
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: This integration does not provide additional actions.
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
@@ -20,6 +20,11 @@
}
},
"entity": {
"button": {
"reset_filter": {
"name": "Reset filter"
}
},
"sensor": {
"indoor_humidity": {
"name": "Indoor humidity"
@@ -31,5 +36,13 @@
"name": "Outdoor temperature"
}
}
},
"exceptions": {
"reset_filter_failed": {
"message": "Failed to reset the filter."
},
"set_fan_mode_failed": {
"message": "Failed to set the ventilation mode."
}
}
}
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.97", "babel==2.15.0"]
"requirements": ["holidays==0.98", "babel==2.15.0"]
}
+24 -19
View File
@@ -108,27 +108,32 @@ def _handle_paired_or_connected_appliance(
)
if entity.unique_id not in known_entity_unique_ids
)
for event_key in (
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
if not (
callbacks_for_appliance := changed_options_listener_remove_callbacks[
appliance_ha_id
]
):
changed_options_listener_remove_callback = (
appliance_coordinator.async_add_listener(
partial(
_create_option_entities,
entity_registry,
appliance_coordinator,
known_entity_unique_ids,
get_option_entities_for_appliance,
async_add_entities,
),
event_key,
for event_key in (
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
):
changed_options_listener_remove_callback = (
appliance_coordinator.async_add_listener(
partial(
_create_option_entities,
entity_registry,
appliance_coordinator,
known_entity_unique_ids,
get_option_entities_for_appliance,
async_add_entities,
),
event_key,
)
)
entry.async_on_unload(changed_options_listener_remove_callback)
callbacks_for_appliance.append(
changed_options_listener_remove_callback
)
)
entry.async_on_unload(changed_options_listener_remove_callback)
changed_options_listener_remove_callbacks[appliance_ha_id].append(
changed_options_listener_remove_callback
)
known_entity_unique_ids.update(
{cast(str, entity.unique_id): appliance_ha_id for entity in entities_to_add}
)
@@ -23,6 +23,6 @@
"iot_class": "cloud_push",
"loggers": ["aiohomeconnect"],
"quality_scale": "platinum",
"requirements": ["aiohomeconnect==0.36.0"],
"requirements": ["aiohomeconnect==0.36.1"],
"zeroconf": ["_homeconnect._tcp.local."]
}
@@ -22,141 +22,147 @@ set_program_and_options:
custom_value: false
translation_key: programs
options:
- consumer_products_cleaning_robot_program_basic_go_home
- consumer_products_cleaning_robot_program_cleaning_clean_all
- consumer_products_cleaning_robot_program_cleaning_clean_map
- consumer_products_cleaning_robot_program_basic_go_home
- consumer_products_coffee_maker_program_beverage_ristretto
- consumer_products_coffee_maker_program_beverage_caffe_grande
- consumer_products_coffee_maker_program_beverage_caffe_latte
- consumer_products_coffee_maker_program_beverage_cappuccino
- consumer_products_coffee_maker_program_beverage_coffee
- consumer_products_coffee_maker_program_beverage_espresso
- consumer_products_coffee_maker_program_beverage_espresso_doppio
- consumer_products_coffee_maker_program_beverage_coffee
- consumer_products_coffee_maker_program_beverage_x_l_coffee
- consumer_products_coffee_maker_program_beverage_caffe_grande
- consumer_products_coffee_maker_program_beverage_espresso_macchiato
- consumer_products_coffee_maker_program_beverage_cappuccino
- consumer_products_coffee_maker_program_beverage_hot_water
- consumer_products_coffee_maker_program_beverage_latte_macchiato
- consumer_products_coffee_maker_program_beverage_caffe_latte
- consumer_products_coffee_maker_program_beverage_milk_froth
- consumer_products_coffee_maker_program_beverage_ristretto
- consumer_products_coffee_maker_program_beverage_warm_milk
- consumer_products_coffee_maker_program_coffee_world_kleiner_brauner
- consumer_products_coffee_maker_program_beverage_x_l_coffee
- consumer_products_coffee_maker_program_coffee_world_americano
- consumer_products_coffee_maker_program_coffee_world_black_eye
- consumer_products_coffee_maker_program_coffee_world_cafe_au_lait
- consumer_products_coffee_maker_program_coffee_world_cafe_con_leche
- consumer_products_coffee_maker_program_coffee_world_cafe_cortado
- consumer_products_coffee_maker_program_coffee_world_cortado
- consumer_products_coffee_maker_program_coffee_world_dead_eye
- consumer_products_coffee_maker_program_coffee_world_doppio
- consumer_products_coffee_maker_program_coffee_world_flat_white
- consumer_products_coffee_maker_program_coffee_world_galao
- consumer_products_coffee_maker_program_coffee_world_garoto
- consumer_products_coffee_maker_program_coffee_world_grosser_brauner
- consumer_products_coffee_maker_program_coffee_world_kaapi
- consumer_products_coffee_maker_program_coffee_world_kleiner_brauner
- consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd
- consumer_products_coffee_maker_program_coffee_world_red_eye
- consumer_products_coffee_maker_program_coffee_world_verlaengerter
- consumer_products_coffee_maker_program_coffee_world_verlaengerter_braun
- consumer_products_coffee_maker_program_coffee_world_wiener_melange
- consumer_products_coffee_maker_program_coffee_world_flat_white
- consumer_products_coffee_maker_program_coffee_world_cortado
- consumer_products_coffee_maker_program_coffee_world_cafe_cortado
- consumer_products_coffee_maker_program_coffee_world_cafe_con_leche
- consumer_products_coffee_maker_program_coffee_world_cafe_au_lait
- consumer_products_coffee_maker_program_coffee_world_doppio
- consumer_products_coffee_maker_program_coffee_world_kaapi
- consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd
- consumer_products_coffee_maker_program_coffee_world_galao
- consumer_products_coffee_maker_program_coffee_world_garoto
- consumer_products_coffee_maker_program_coffee_world_americano
- consumer_products_coffee_maker_program_coffee_world_red_eye
- consumer_products_coffee_maker_program_coffee_world_black_eye
- consumer_products_coffee_maker_program_coffee_world_dead_eye
- consumer_products_coffee_maker_program_beverage_hot_water
- dishcare_dishwasher_program_pre_rinse
- cooking_common_program_hood_automatic
- cooking_common_program_hood_delayed_shut_off
- cooking_common_program_hood_venting
- cooking_oven_program_heating_mode_3_d_hot_air
- cooking_oven_program_heating_mode_air_fry
- cooking_oven_program_heating_mode_bottom_heating
- cooking_oven_program_heating_mode_bread_baking
- cooking_oven_program_heating_mode_defrost
- cooking_oven_program_heating_mode_desiccation
- cooking_oven_program_heating_mode_dough_proving
- cooking_oven_program_heating_mode_frozen_heatup_special
- cooking_oven_program_heating_mode_grill_large_area
- cooking_oven_program_heating_mode_grill_small_area
- cooking_oven_program_heating_mode_hot_air
- cooking_oven_program_heating_mode_hot_air_100_steam
- cooking_oven_program_heating_mode_hot_air_30_steam
- cooking_oven_program_heating_mode_hot_air_60_steam
- cooking_oven_program_heating_mode_hot_air_80_steam
- cooking_oven_program_heating_mode_hot_air_eco
- cooking_oven_program_heating_mode_hot_air_gentle
- cooking_oven_program_heating_mode_hot_air_grilling
- cooking_oven_program_heating_mode_intensive_heat
- cooking_oven_program_heating_mode_keep_warm
- cooking_oven_program_heating_mode_pizza_setting
- cooking_oven_program_heating_mode_pre_heating
- cooking_oven_program_heating_mode_preheat_ovenware
- cooking_oven_program_heating_mode_proof
- cooking_oven_program_heating_mode_sabbath_programme
- cooking_oven_program_heating_mode_slow_cook
- cooking_oven_program_heating_mode_top_bottom_heating
- cooking_oven_program_heating_mode_top_bottom_heating_eco
- cooking_oven_program_heating_mode_warming_drawer
- cooking_oven_program_microwave_1000_watt
- cooking_oven_program_microwave_180_watt
- cooking_oven_program_microwave_360_watt
- cooking_oven_program_microwave_450_watt
- cooking_oven_program_microwave_600_watt
- cooking_oven_program_microwave_900_watt
- cooking_oven_program_microwave_90_watt
- cooking_oven_program_microwave_max
- cooking_oven_program_steam_modes_steam
- dishcare_dishwasher_program_auto_1
- dishcare_dishwasher_program_auto_2
- dishcare_dishwasher_program_auto_3
- dishcare_dishwasher_program_auto_half_load
- dishcare_dishwasher_program_eco_50
- dishcare_dishwasher_program_quick_45
- dishcare_dishwasher_program_intensiv_70
- dishcare_dishwasher_program_normal_65
- dishcare_dishwasher_program_express_sparkle_65
- dishcare_dishwasher_program_glas_40
- dishcare_dishwasher_program_glass_care
- dishcare_dishwasher_program_night_wash
- dishcare_dishwasher_program_quick_65
- dishcare_dishwasher_program_normal_45
- dishcare_dishwasher_program_intensiv_45
- dishcare_dishwasher_program_auto_half_load
- dishcare_dishwasher_program_intensiv_70
- dishcare_dishwasher_program_intensiv_power
- dishcare_dishwasher_program_intensive_fixed_zone
- dishcare_dishwasher_program_magic_daily
- dishcare_dishwasher_program_super_60
- dishcare_dishwasher_program_kurz_60
- dishcare_dishwasher_program_express_sparkle_65
- dishcare_dishwasher_program_learning_dishwasher
- dishcare_dishwasher_program_machine_care
- dishcare_dishwasher_program_steam_fresh
- dishcare_dishwasher_program_magic_daily
- dishcare_dishwasher_program_maximum_cleaning
- dishcare_dishwasher_program_mixed_load
- dishcare_dishwasher_program_learning_dishwasher
- dishcare_dishwasher_program_night_wash
- dishcare_dishwasher_program_normal_45
- dishcare_dishwasher_program_normal_65
- dishcare_dishwasher_program_pre_rinse
- dishcare_dishwasher_program_quick_45
- dishcare_dishwasher_program_quick_65
- dishcare_dishwasher_program_steam_fresh
- dishcare_dishwasher_program_super_60
- heating_ventilation_air_conditioning_air_conditioner_program_active_clean
- heating_ventilation_air_conditioning_air_conditioner_program_auto
- heating_ventilation_air_conditioning_air_conditioner_program_cool
- heating_ventilation_air_conditioning_air_conditioner_program_dry
- heating_ventilation_air_conditioning_air_conditioner_program_fan
- heating_ventilation_air_conditioning_air_conditioner_program_heat
- laundry_care_dryer_program_cotton
- laundry_care_dryer_program_synthetic
- laundry_care_dryer_program_mix
- laundry_care_dryer_program_anti_shrink
- laundry_care_dryer_program_blankets
- laundry_care_dryer_program_business_shirts
- laundry_care_dryer_program_cotton
- laundry_care_dryer_program_delicates
- laundry_care_dryer_program_dessous
- laundry_care_dryer_program_down_feathers
- laundry_care_dryer_program_hygiene
- laundry_care_dryer_program_jeans
- laundry_care_dryer_program_outdoor
- laundry_care_dryer_program_synthetic_refresh
- laundry_care_dryer_program_towels
- laundry_care_dryer_program_delicates
- laundry_care_dryer_program_super_40
- laundry_care_dryer_program_shirts_15
- laundry_care_dryer_program_pillow
- laundry_care_dryer_program_anti_shrink
- laundry_care_dryer_program_my_time_my_drying_time
- laundry_care_dryer_program_time_cold
- laundry_care_dryer_program_time_warm
- laundry_care_dryer_program_in_basket
- laundry_care_dryer_program_jeans
- laundry_care_dryer_program_mix
- laundry_care_dryer_program_my_time_my_drying_time
- laundry_care_dryer_program_outdoor
- laundry_care_dryer_program_pillow
- laundry_care_dryer_program_shirts_15
- laundry_care_dryer_program_super_40
- laundry_care_dryer_program_synthetic
- laundry_care_dryer_program_synthetic_refresh
- laundry_care_dryer_program_time_cold
- laundry_care_dryer_program_time_cold_fix_time_cold_20
- laundry_care_dryer_program_time_cold_fix_time_cold_30
- laundry_care_dryer_program_time_cold_fix_time_cold_60
- laundry_care_dryer_program_time_warm
- laundry_care_dryer_program_time_warm_fix_time_warm_30
- laundry_care_dryer_program_time_warm_fix_time_warm_40
- laundry_care_dryer_program_time_warm_fix_time_warm_60
- laundry_care_dryer_program_dessous
- cooking_common_program_hood_automatic
- cooking_common_program_hood_venting
- cooking_common_program_hood_delayed_shut_off
- cooking_oven_program_heating_mode_3_d_hot_air
- cooking_oven_program_heating_mode_air_fry
- cooking_oven_program_heating_mode_grill_large_area
- cooking_oven_program_heating_mode_grill_small_area
- cooking_oven_program_heating_mode_pre_heating
- cooking_oven_program_heating_mode_hot_air
- cooking_oven_program_heating_mode_hot_air_eco
- cooking_oven_program_heating_mode_hot_air_gentle
- cooking_oven_program_heating_mode_hot_air_grilling
- cooking_oven_program_heating_mode_top_bottom_heating
- cooking_oven_program_heating_mode_top_bottom_heating_eco
- cooking_oven_program_heating_mode_bottom_heating
- cooking_oven_program_heating_mode_bread_baking
- cooking_oven_program_heating_mode_pizza_setting
- cooking_oven_program_heating_mode_slow_cook
- cooking_oven_program_heating_mode_intensive_heat
- cooking_oven_program_heating_mode_keep_warm
- cooking_oven_program_heating_mode_preheat_ovenware
- cooking_oven_program_heating_mode_frozen_heatup_special
- cooking_oven_program_heating_mode_desiccation
- cooking_oven_program_heating_mode_defrost
- cooking_oven_program_heating_mode_dough_proving
- cooking_oven_program_heating_mode_proof
- cooking_oven_program_heating_mode_hot_air_30_steam
- cooking_oven_program_heating_mode_hot_air_60_steam
- cooking_oven_program_heating_mode_hot_air_80_steam
- cooking_oven_program_heating_mode_hot_air_100_steam
- cooking_oven_program_heating_mode_sabbath_programme
- cooking_oven_program_microwave_90_watt
- cooking_oven_program_microwave_180_watt
- cooking_oven_program_microwave_360_watt
- cooking_oven_program_microwave_450_watt
- cooking_oven_program_microwave_600_watt
- cooking_oven_program_microwave_900_watt
- cooking_oven_program_microwave_1000_watt
- cooking_oven_program_microwave_max
- cooking_oven_program_steam_modes_steam
- cooking_oven_program_heating_mode_warming_drawer
- laundry_care_dryer_program_towels
- laundry_care_washer_dryer_program_cotton
- laundry_care_washer_dryer_program_cotton_eco_4060
- laundry_care_washer_dryer_program_easy_care
- laundry_care_washer_dryer_program_mix
- laundry_care_washer_dryer_program_wash_and_dry_60
- laundry_care_washer_dryer_program_wash_and_dry_90
- laundry_care_washer_program_auto_30
- laundry_care_washer_program_auto_40
- laundry_care_washer_program_auto_60
@@ -190,12 +196,6 @@ set_program_and_options:
- laundry_care_washer_program_towels
- laundry_care_washer_program_water_proof
- laundry_care_washer_program_wool
- laundry_care_washer_dryer_program_cotton
- laundry_care_washer_dryer_program_cotton_eco_4060
- laundry_care_washer_dryer_program_mix
- laundry_care_washer_dryer_program_easy_care
- laundry_care_washer_dryer_program_wash_and_dry_60
- laundry_care_washer_dryer_program_wash_and_dry_90
air_conditioner_options:
collapsed: true
fields:
+58 -22
View File
@@ -1,5 +1,6 @@
"""The homee cover platform."""
from enum import Enum
import logging
from typing import TYPE_CHECKING, Any, cast
@@ -24,12 +25,6 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
OPEN_CLOSE_ATTRIBUTES = [
AttributeType.OPEN_CLOSE,
AttributeType.SLAT_ROTATION_IMPULSE,
AttributeType.UP_DOWN,
]
POSITION_ATTRIBUTES = [AttributeType.POSITION, AttributeType.SHUTTER_SLAT_POSITION]
COVER_DEVICE_PROFILES = {
NodeProfile.GARAGE_DOOR_OPERATOR: CoverDeviceClass.GARAGE,
NodeProfile.ENTRANCE_GATE_OPERATOR: CoverDeviceClass.GATE,
@@ -43,6 +38,24 @@ IS_CLOSED_ATTRIBUTES = [
]
class HomeeCoverState(float, Enum):
"""Open/closed states for covers in homee."""
OPEN = 0.0
CLOSED = 1.0
STOPPED = 2.0
OPENING = 3.0
CLOSING = 4.0
class HomeeSlatState(float, Enum):
"""Slat states for covers in homee."""
STOPPED = 0.0
CLOSED = 1.0
OPEN = 2.0
def get_open_close_attribute(node: HomeeNode) -> HomeeAttribute | None:
"""Return the attribute used for opening/closing the cover."""
# We assume, that no device has UP_DOWN and OPEN_CLOSE, but only one of them.
@@ -70,7 +83,11 @@ def get_cover_features(
features |= CoverEntityFeature.SET_POSITION
if node.get_attribute_by_type(AttributeType.SLAT_ROTATION_IMPULSE) is not None:
features |= CoverEntityFeature.OPEN_TILT | CoverEntityFeature.CLOSE_TILT
features |= (
CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.STOP_TILT
)
if node.get_attribute_by_type(AttributeType.SHUTTER_SLAT_POSITION) is not None:
features |= CoverEntityFeature.SET_TILT_POSITION
@@ -187,9 +204,9 @@ class HomeeCover(HomeeNodeEntity, CoverEntity):
"""Return the opening status of the cover."""
if self._open_close_attribute is not None:
return (
self._open_close_attribute.get_value() == 3
self._open_close_attribute.get_value() == HomeeCoverState.OPENING
if not self._open_close_attribute.is_reversed
else self._open_close_attribute.get_value() == 4
else self._open_close_attribute.get_value() == HomeeCoverState.CLOSING
)
return None
@@ -199,9 +216,9 @@ class HomeeCover(HomeeNodeEntity, CoverEntity):
"""Return the closing status of the cover."""
if self._open_close_attribute is not None:
return (
self._open_close_attribute.get_value() == 4
self._open_close_attribute.get_value() == HomeeCoverState.CLOSING
if not self._open_close_attribute.is_reversed
else self._open_close_attribute.get_value() == 3
else self._open_close_attribute.get_value() == HomeeCoverState.OPENING
)
return None
@@ -216,9 +233,9 @@ class HomeeCover(HomeeNodeEntity, CoverEntity):
if self._open_close_attribute is not None:
if not self._open_close_attribute.is_reversed:
return self._open_close_attribute.get_value() == 1
return self._open_close_attribute.get_value() == HomeeCoverState.CLOSED
return self._open_close_attribute.get_value() == 0
return self._open_close_attribute.get_value() == HomeeCoverState.OPEN
# If none of the above is present, it will be a slat only cover.
attribute = self._node.get_attribute_by_type(
@@ -235,17 +252,25 @@ class HomeeCover(HomeeNodeEntity, CoverEntity):
"""Open the cover."""
assert self._open_close_attribute is not None
if not self._open_close_attribute.is_reversed:
await self.async_set_homee_value(self._open_close_attribute, 0)
await self.async_set_homee_value(
self._open_close_attribute, HomeeCoverState.OPEN
)
else:
await self.async_set_homee_value(self._open_close_attribute, 1)
await self.async_set_homee_value(
self._open_close_attribute, HomeeCoverState.CLOSED
)
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close cover."""
assert self._open_close_attribute is not None
if not self._open_close_attribute.is_reversed:
await self.async_set_homee_value(self._open_close_attribute, 1)
await self.async_set_homee_value(
self._open_close_attribute, HomeeCoverState.CLOSED
)
else:
await self.async_set_homee_value(self._open_close_attribute, 0)
await self.async_set_homee_value(
self._open_close_attribute, HomeeCoverState.OPEN
)
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
@@ -265,7 +290,9 @@ class HomeeCover(HomeeNodeEntity, CoverEntity):
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
if self._open_close_attribute is not None:
await self.async_set_homee_value(self._open_close_attribute, 2)
await self.async_set_homee_value(
self._open_close_attribute, HomeeCoverState.STOPPED
)
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt."""
@@ -275,9 +302,9 @@ class HomeeCover(HomeeNodeEntity, CoverEntity):
)
) is not None:
if not slat_attribute.is_reversed:
await self.async_set_homee_value(slat_attribute, 2)
await self.async_set_homee_value(slat_attribute, HomeeSlatState.OPEN)
else:
await self.async_set_homee_value(slat_attribute, 1)
await self.async_set_homee_value(slat_attribute, HomeeSlatState.CLOSED)
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the cover tilt."""
@@ -287,9 +314,18 @@ class HomeeCover(HomeeNodeEntity, CoverEntity):
)
) is not None:
if not slat_attribute.is_reversed:
await self.async_set_homee_value(slat_attribute, 1)
await self.async_set_homee_value(slat_attribute, HomeeSlatState.CLOSED)
else:
await self.async_set_homee_value(slat_attribute, 2)
await self.async_set_homee_value(slat_attribute, HomeeSlatState.OPEN)
async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
"""Stop the cover tilt."""
if (
slat_attribute := self._node.get_attribute_by_type(
AttributeType.SLAT_ROTATION_IMPULSE
)
) is not None:
await self.async_set_homee_value(slat_attribute, HomeeSlatState.STOPPED)
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position."""
@@ -9,5 +9,5 @@
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",
"requirements": ["aioautomower==2.7.5"]
"requirements": ["aioautomower==2.7.6"]
}
@@ -13,5 +13,5 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["idasen-ha==2.6.5"]
"requirements": ["idasen-ha==2.7.0"]
}
+19 -23
View File
@@ -2,20 +2,21 @@
import asyncio
import collections
from collections.abc import Container, Mapping
from contextlib import suppress
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
import os
from random import SystemRandom
from typing import Final, final
from typing import Final, final, override
from aiohttp import hdrs, web
from aiohttp import web
import httpx
from propcache.api import cached_property
import voluptuous as vol
from homeassistant.components.http import KEY_AUTHENTICATED, KEY_HASS, HomeAssistantView
from homeassistant.components.http import KEY_HASS, HomeAssistantView
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONTENT_TYPE_MULTIPART, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import (
@@ -314,33 +315,28 @@ class ImageView(HomeAssistantView):
"""View to serve an image."""
name = "api:image:image"
requires_auth = False
use_query_token_for_auth = True
url = "/api/image_proxy/{entity_id}"
def __init__(self, component: EntityComponent[ImageEntity]) -> None:
"""Initialize an image view."""
self.component = component
async def _authenticate_request(
self, request: web.Request, entity_id: str
) -> ImageEntity:
"""Authenticate request and return image entity."""
@callback
@override
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
"""Return valid auth tokens, which can be used for query token authentication."""
if (image_entity := self.component.get_entity(match_info["entity_id"])) is None:
return ()
return image_entity.access_tokens
@callback
def _get_image_entity(self, entity_id: str) -> ImageEntity:
"""Get image entity from request."""
if (image_entity := self.component.get_entity(entity_id)) is None:
raise web.HTTPNotFound
authenticated = (
request[KEY_AUTHENTICATED]
or request.query.get("token") in image_entity.access_tokens
)
if not authenticated:
# Attempt with invalid bearer token, raise unauthorized
# so ban middleware can handle it.
if hdrs.AUTHORIZATION in request.headers:
raise web.HTTPUnauthorized
# Invalid sigAuth or image entity access token
raise web.HTTPForbidden
return image_entity
async def head(self, request: web.Request, entity_id: str) -> web.Response:
@@ -349,7 +345,7 @@ class ImageView(HomeAssistantView):
This is sent by some DLNA renderers, like Samsung ones, prior to sending
the GET request.
"""
image_entity = await self._authenticate_request(request, entity_id)
image_entity = self._get_image_entity(entity_id)
# Don't use `handle` as we don't care about the stream case, we only want
# to verify that the image exists.
@@ -365,7 +361,7 @@ class ImageView(HomeAssistantView):
async def get(self, request: web.Request, entity_id: str) -> web.StreamResponse:
"""Start a GET request."""
image_entity = await self._authenticate_request(request, entity_id)
image_entity = self._get_image_entity(entity_id)
return await self.handle(request, image_entity)
async def handle(
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["imgw_pib==2.2.0"]
"requirements": ["imgw_pib==2.2.2"]
}
+42
View File
@@ -0,0 +1,42 @@
"""Support for Imou devices."""
from pyimouapi.device import ImouDeviceManager
from pyimouapi.ha_device import ImouHaDeviceManager
from pyimouapi.openapi import ImouOpenApiClient
from homeassistant.core import HomeAssistant, callback
from .const import API_URLS, CONF_API_URL, CONF_APP_ID, CONF_APP_SECRET, PLATFORMS
from .coordinator import ImouConfigEntry, ImouDataUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ImouConfigEntry) -> bool:
"""Set up Imou integration from a config entry."""
imou_client = ImouOpenApiClient(
entry.data[CONF_APP_ID],
entry.data[CONF_APP_SECRET],
API_URLS[entry.data[CONF_API_URL]],
)
device_manager = ImouDeviceManager(imou_client)
imou_device_manager = ImouHaDeviceManager(device_manager)
imou_coordinator = ImouDataUpdateCoordinator(hass, imou_device_manager, entry)
await imou_coordinator.async_config_entry_first_refresh()
entry.runtime_data = imou_coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# DataUpdateCoordinator schedules periodic refreshes only when it has
# listeners. With zero entities (e.g. an empty account at setup), register a
# no-op listener so polling continues and later devices are discovered via
# new_device_callbacks.
@callback
def _async_keep_polling() -> None:
"""Keep periodic polling when no entities are registered yet."""
entry.async_on_unload(imou_coordinator.async_add_listener(_async_keep_polling))
return True
async def async_unload_entry(hass: HomeAssistant, entry: ImouConfigEntry) -> bool:
"""Handle removal of an entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+109
View File
@@ -0,0 +1,109 @@
"""Support for Imou button controls."""
from pyimouapi.exceptions import ImouException
from pyimouapi.ha_device import ImouHaDevice
from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import PTZ_MOVE_DURATION_MS, imou_device_identifier
from .coordinator import ImouConfigEntry, ImouDataUpdateCoordinator
from .entity import ImouEntity
PARALLEL_UPDATES = 1
# Button types
PARAM_RESTART_DEVICE = "restart_device"
PARAM_MUTE = "mute"
PARAM_PTZ_UP = "ptz_up"
PARAM_PTZ_DOWN = "ptz_down"
PARAM_PTZ_LEFT = "ptz_left"
PARAM_PTZ_RIGHT = "ptz_right"
BUTTON_TYPES = (
PARAM_RESTART_DEVICE,
PARAM_MUTE,
PARAM_PTZ_UP,
PARAM_PTZ_DOWN,
PARAM_PTZ_LEFT,
PARAM_PTZ_RIGHT,
)
PTZ_BUTTON_TYPES = (
PARAM_PTZ_UP,
PARAM_PTZ_DOWN,
PARAM_PTZ_LEFT,
PARAM_PTZ_RIGHT,
)
BUTTON_DEVICE_CLASS: dict[str, ButtonDeviceClass] = {
PARAM_RESTART_DEVICE: ButtonDeviceClass.RESTART,
}
def _iter_buttons(
coordinator: ImouDataUpdateCoordinator,
) -> list[tuple[str, ImouHaDevice]]:
"""Return (button_type, device) pairs for supported buttons."""
return [
(button_type, device)
for device in coordinator.devices
for button_type in device.buttons
if button_type in BUTTON_TYPES
]
async def async_setup_entry(
hass: HomeAssistant,
entry: ImouConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Imou button entities."""
coordinator = entry.runtime_data
def _add_buttons(new_devices: list[ImouHaDevice]) -> None:
device_keys = {imou_device_identifier(device) for device in new_devices}
async_add_entities(
ImouButton(coordinator, button_type, device)
for button_type, device in _iter_buttons(coordinator)
if imou_device_identifier(device) in device_keys
)
coordinator.new_device_callbacks.append(_add_buttons)
@callback
def _remove_new_device_callback() -> None:
if _add_buttons in coordinator.new_device_callbacks:
coordinator.new_device_callbacks.remove(_add_buttons)
entry.async_on_unload(_remove_new_device_callback)
_add_buttons(coordinator.devices)
class ImouButton(ImouEntity, ButtonEntity):
"""Imou button entity."""
def __init__(
self,
coordinator: ImouDataUpdateCoordinator,
entity_type: str,
device: ImouHaDevice,
) -> None:
"""Initialize the Imou button entity."""
super().__init__(coordinator, entity_type, device)
if device_class := BUTTON_DEVICE_CLASS.get(entity_type):
self._attr_device_class = device_class
self._attr_translation_key = None
async def async_press(self) -> None:
"""Handle button press."""
duration = PTZ_MOVE_DURATION_MS if self._entity_type in PTZ_BUTTON_TYPES else 0
try:
await self.coordinator.device_manager.async_press_button(
self.device,
self._entity_type,
duration,
)
except ImouException as e:
raise HomeAssistantError(str(e)) from e
@@ -0,0 +1,80 @@
"""Config flow for Imou."""
import logging
from typing import Any
from pyimouapi.exceptions import (
ConnectFailedException,
ImouException,
InvalidAppIdOrSecretException,
RequestFailedException,
)
from pyimouapi.openapi import ImouOpenApiClient
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
from .const import API_URLS, CONF_API_URL, CONF_APP_ID, CONF_APP_SECRET, DOMAIN
_LOGGER = logging.getLogger(__name__)
class ImouConfigFlow(ConfigFlow, domain=DOMAIN):
"""Config flow for Imou integration."""
VERSION = 1
MINOR_VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step of the config flow."""
errors: dict[str, str] = {}
if user_input is not None:
await self.async_set_unique_id(user_input[CONF_APP_ID])
self._abort_if_unique_id_configured()
api_client = ImouOpenApiClient(
user_input[CONF_APP_ID],
user_input[CONF_APP_SECRET],
API_URLS[user_input[CONF_API_URL]],
)
try:
await api_client.async_get_token()
except InvalidAppIdOrSecretException:
errors["base"] = "invalid_auth"
except ConnectFailedException, RequestFailedException:
errors["base"] = "cannot_connect"
except ImouException as exception:
_LOGGER.debug("Imou error during config flow: %s", exception)
errors["base"] = "unknown"
else:
return self.async_create_entry(
title="Imou",
data={
CONF_APP_ID: user_input[CONF_APP_ID],
CONF_APP_SECRET: user_input[CONF_APP_SECRET],
CONF_API_URL: user_input[CONF_API_URL],
},
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_APP_ID): str,
vol.Required(CONF_APP_SECRET): str,
vol.Required(CONF_API_URL, default="sg"): SelectSelector(
SelectSelectorConfig(
options=list(API_URLS),
translation_key="api_url",
mode=SelectSelectorMode.DROPDOWN,
)
),
}
),
errors=errors,
)
+39
View File
@@ -0,0 +1,39 @@
"""Constants."""
from pyimouapi.ha_device import ImouHaDevice
from homeassistant.const import Platform
DOMAIN = "imou"
def imou_device_identifier(device: ImouHaDevice) -> str:
"""Return a device registry identifier (device_id + channel when present)."""
if device.channel_id is not None:
return f"{device.device_id}_{device.channel_id}"
return device.device_id
# API URL region mapping
API_URLS: dict[str, str] = {
"sg": "openapi-sg.easy4ip.com",
"eu": "openapi-or.easy4ip.com",
"na": "openapi-fk.easy4ip.com",
"cn": "openapi.lechange.cn",
}
CONF_API_URL = "api_url"
CONF_APP_ID = "app_id"
CONF_APP_SECRET = "app_secret"
PARAM_STATUS = "status"
PARAM_STATE = "state"
# How long each PTZ button press moves the camera, in milliseconds (Imou cloud API).
PTZ_MOVE_DURATION_MS = 500
# Upper bound for a full coordinator refresh (device list + status for all devices).
UPDATE_TIMEOUT = 300
PLATFORMS = [Platform.BUTTON]
@@ -0,0 +1,152 @@
"""Provides the Imou DataUpdateCoordinator."""
import asyncio
from collections.abc import Callable
from datetime import timedelta
import logging
from pyimouapi.exceptions import ImouException
from pyimouapi.ha_device import ImouHaDevice, ImouHaDeviceManager
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, UPDATE_TIMEOUT, imou_device_identifier
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=120)
type ImouConfigEntry = ConfigEntry[ImouDataUpdateCoordinator]
class ImouDataUpdateCoordinator(DataUpdateCoordinator[None]):
"""Data update coordinator for Imou devices."""
config_entry: ImouConfigEntry
def __init__(
self,
hass: HomeAssistant,
device_manager: ImouHaDeviceManager,
config_entry: ImouConfigEntry,
) -> None:
"""Initialize the Imou data update coordinator."""
super().__init__(
hass,
_LOGGER,
name="ImouDataUpdateCoordinator",
update_interval=SCAN_INTERVAL,
config_entry=config_entry,
always_update=True,
)
self._device_manager = device_manager
self.devices_by_key: dict[str, ImouHaDevice] = {}
self._devices_initialized = False
self.new_device_callbacks: list[Callable[[list[ImouHaDevice]], None]] = []
@property
def devices(self) -> list[ImouHaDevice]:
"""Return the list of devices."""
return list(self.devices_by_key.values())
@property
def device_manager(self) -> ImouHaDeviceManager:
"""Return the device manager."""
return self._device_manager
def get_device(self, device_key: str) -> ImouHaDevice | None:
"""Return the current device for device_key, if still on the account."""
return self.devices_by_key.get(device_key)
async def _async_update_data(self) -> None:
"""Update coordinator data."""
try:
async with asyncio.timeout(UPDATE_TIMEOUT):
fresh_devices = await self._device_manager.async_get_devices()
except TimeoutError as err:
raise UpdateFailed(f"Timeout while fetching data: {err}") from err
except ImouException as err:
raise UpdateFailed(f"Error fetching Imou devices: {err}") from err
fresh_by_key = {
imou_device_identifier(device): device for device in fresh_devices
}
self._async_add_remove_devices(fresh_by_key)
devices = list(self.devices_by_key.values())
if not devices:
return
try:
async with asyncio.timeout(UPDATE_TIMEOUT):
results = await asyncio.gather(
*(
self._device_manager.async_update_device_status(device)
for device in devices
),
return_exceptions=True,
)
except TimeoutError as err:
raise UpdateFailed(f"Timeout while fetching data: {err}") from err
failures: list[Exception] = []
for device, result in zip(devices, results, strict=True):
if isinstance(result, BaseException) and not isinstance(result, Exception):
# Propagate CancelledError and other BaseExceptions instead of
# swallowing them as a regular device failure.
raise result
if not isinstance(result, Exception):
continue
device_key = imou_device_identifier(device)
_LOGGER.warning(
"Error updating status for Imou device %s: %s",
device_key,
result,
)
failures.append(result)
if failures and len(failures) == len(devices):
raise UpdateFailed(
f"Error updating Imou devices: {failures[0]}"
) from failures[0]
def _async_add_remove_devices(self, fresh_by_key: dict[str, ImouHaDevice]) -> None:
"""Add new devices, remove devices no longer in the account.
This only tracks which devices exist on the account; per-device state
is updated in place by `async_update_device_status`, so devices that
remain on the account keep their existing object and are not replaced.
"""
if not self._devices_initialized:
self.devices_by_key = fresh_by_key
self._devices_initialized = True
return
current_keys = set(fresh_by_key)
known_keys = set(self.devices_by_key)
if current_keys == known_keys:
return
if removed_keys := known_keys - current_keys:
_LOGGER.debug("Removed Imou device(s): %s", ", ".join(removed_keys))
device_registry = dr.async_get(self.hass)
for device_key in removed_keys:
del self.devices_by_key[device_key]
if device := device_registry.async_get_device(
identifiers={(DOMAIN, device_key)}
):
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
if new_keys := current_keys - known_keys:
_LOGGER.debug("New Imou device(s) found: %s", ", ".join(new_keys))
new_devices = []
for device_key in new_keys:
self.devices_by_key[device_key] = fresh_by_key[device_key]
new_devices.append(fresh_by_key[device_key])
for callback in self.new_device_callbacks:
callback(new_devices)
+59
View File
@@ -0,0 +1,59 @@
"""An abstract class common to all Imou entities."""
from pyimouapi.ha_device import DeviceStatus, ImouHaDevice
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, PARAM_STATE, PARAM_STATUS, imou_device_identifier
from .coordinator import ImouDataUpdateCoordinator
class ImouEntity(CoordinatorEntity[ImouDataUpdateCoordinator]):
"""Base class for all Imou entities."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: ImouDataUpdateCoordinator,
entity_type: str,
device: ImouHaDevice,
) -> None:
"""Initialize the Imou entity."""
super().__init__(coordinator)
self._entity_type = entity_type
self._device_key = imou_device_identifier(device)
self._attr_unique_id = f"{self._device_key}${entity_type}"
self._attr_translation_key = entity_type
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._device_key)},
name=device.channel_name or device.device_name,
manufacturer=device.manufacturer,
model=device.model,
sw_version=device.swversion,
serial_number=device.device_id,
)
@property
def device(self) -> ImouHaDevice:
"""Return the live device from the coordinator.
Callers must guard with `available` first; accessing this for a device
that has left the account raises `KeyError`.
"""
return self.coordinator.devices_by_key[self._device_key]
@property
def available(self) -> bool:
"""Return if the entity is available."""
if (
not super().available
or self._device_key not in self.coordinator.devices_by_key
):
return False
if PARAM_STATUS not in self.device.sensors:
return False
return (
self.device.sensors[PARAM_STATUS][PARAM_STATE] != DeviceStatus.OFFLINE.value
)
+18
View File
@@ -0,0 +1,18 @@
{
"entity": {
"button": {
"ptz_down": {
"default": "mdi:arrow-down-bold"
},
"ptz_left": {
"default": "mdi:arrow-left-bold"
},
"ptz_right": {
"default": "mdi:arrow-right-bold"
},
"ptz_up": {
"default": "mdi:arrow-up-bold"
}
}
}
}
@@ -0,0 +1,11 @@
{
"domain": "imou",
"name": "Imou",
"codeowners": ["@Imou-OpenPlatform"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/imou",
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["pyimouapi==1.2.7"]
}

Some files were not shown because too many files have changed in this diff Show More