Compare commits

...

88 Commits

Author SHA1 Message Date
Anthony Garera
0bed9c20b3 Bump python-overseerr to 0.8.0 (#158924) 2025-12-13 19:31:21 +01:00
Brett Adams
d3fb7a7b87 Bump tesla-fleet-api to 1.2.7 (#158904) 2025-12-13 15:02:19 +01:00
Bouwe Westerdijk
60dcca4143 Show Plugwise configuration-link on gateway only (#158094) 2025-12-13 11:38:23 +01:00
Paul Tarjan
01f498f239 Clarify previous state in total_increasing warning message (#158805) 2025-12-13 11:15:37 +01:00
Andre Lengwenus
15055b8e8e Fix race condition in LCN climate and cover entites (#158894) 2025-12-13 11:12:20 +01:00
Bouwe Westerdijk
6826619e12 Revert adding entity_category to Plugwise thermostat schedule select (#158901) 2025-12-13 11:08:17 +01:00
Joost Lekkerkerker
b50a8e04a8 Add integration_type hub to airtouch5 (#158834)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-13 10:47:27 +01:00
Joost Lekkerkerker
c6c67c5357 Add integration_type hub to blue_current (#158863) 2025-12-13 10:46:12 +01:00
Joost Lekkerkerker
c82803d1e2 Add integration_type hub to agent_dvr (#158829)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-13 10:45:09 +01:00
Joost Lekkerkerker
732b30f181 Add integration_type hub to airzone (#158835)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-13 10:44:05 +01:00
Joost Lekkerkerker
0e2e57a657 Add integration_type device to android_ip_webcam (#158838)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-13 10:42:39 +01:00
Joost Lekkerkerker
f00b0080a9 Add integration_type device to advantage_air (#158826)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-13 10:39:58 +01:00
Joost Lekkerkerker
ad970c1234 Add integration_type hub to cert_expiry (#158897) 2025-12-13 10:39:14 +01:00
Joost Lekkerkerker
02ec56bffa Add integration_type device to ccm15 (#158896)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-13 10:37:36 +01:00
Joost Lekkerkerker
8388c290bf Add integration_type hub to canary (#158895) 2025-12-13 09:41:01 +01:00
Joost Lekkerkerker
576ee99faf Add integration_type hub to control4 (#158900) 2025-12-13 09:36:36 +01:00
Joost Lekkerkerker
8a3534c345 Add integration_type service to coinbase (#158899) 2025-12-13 09:31:54 +01:00
Joost Lekkerkerker
e1e91c5568 Add integration_type service to cloudflare (#158898) 2025-12-13 09:31:25 +01:00
epenet
1e09bddb1d Cleanup deprecated alias in core (#158799) 2025-12-13 09:29:15 +01:00
Joost Lekkerkerker
90e4340595 Add integration_type hub to brunt (#158870)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-12-13 09:04:05 +01:00
Joost Lekkerkerker
120b17349c Add integration_type service to aussie_broadband (#158853) 2025-12-13 08:56:35 +01:00
Joost Lekkerkerker
8a26961304 Add integration_type service to aurora (#158852) 2025-12-13 08:56:01 +01:00
Joost Lekkerkerker
407b675080 Add integration_type device to atag (#158850) 2025-12-13 08:55:35 +01:00
Joost Lekkerkerker
274844271b Add integration_type hub to aseko_pool_live (#158849) 2025-12-13 08:54:46 +01:00
Joost Lekkerkerker
f11e4e7bda Add integration_type hub to aosmith (#158843) 2025-12-13 08:52:45 +01:00
Joost Lekkerkerker
96f8c39c6f Add integration_type device to anthemav (#158841) 2025-12-13 08:51:25 +01:00
Joost Lekkerkerker
77b79fef8d Add integration_type hub to anova (#158840) 2025-12-13 08:50:24 +01:00
mkmer
a0d2f285f3 blink: Remove mkmer as codeowner (#158884) 2025-12-13 08:45:13 +01:00
Joost Lekkerkerker
3aef05d1ec Add integration_type hub to airzone_cloud (#158836) 2025-12-13 08:43:57 +01:00
Joost Lekkerkerker
510e391ee4 Add integration_type device to airtouch4 (#158833) 2025-12-13 08:41:17 +01:00
Joost Lekkerkerker
54adfdd694 Add integration_type device to bluesound (#158865) 2025-12-13 08:38:48 +01:00
Joost Lekkerkerker
d45f920b4a Add integration_type service to amberelectric (#158837) 2025-12-13 08:37:16 +01:00
Joost Lekkerkerker
3080ef9a4a Add integration_type device to airthings_ble (#158832) 2025-12-13 08:36:06 +01:00
Joost Lekkerkerker
51cebb52f3 Add integration_type hub to airthings (#158831) 2025-12-13 08:34:28 +01:00
Joost Lekkerkerker
7b0d4c47b7 Add integration_type service to airnow (#158830) 2025-12-13 08:33:53 +01:00
Joost Lekkerkerker
a660ab3f97 Add integration_type service to aftership (#158828) 2025-12-13 08:32:31 +01:00
Joost Lekkerkerker
dd8fc16788 Add integration_type service to aemet (#158827) 2025-12-13 08:32:01 +01:00
Joost Lekkerkerker
2b0fab0468 Add integration_type service to brottsplatskartan (#158869) 2025-12-13 08:30:59 +01:00
Joost Lekkerkerker
3bb88ed433 Add integration_type hub to bosch_shc (#158868) 2025-12-13 08:30:04 +01:00
Joost Lekkerkerker
984385cd98 Add integration_type service to buienradar (#158871) 2025-12-13 08:27:55 +01:00
Joost Lekkerkerker
09de108676 Add integration_type service to caldav (#158872) 2025-12-13 08:26:40 +01:00
Joost Lekkerkerker
ebc7581718 Add integration_type hub to bmw_connected_drive (#158866) 2025-12-13 08:26:16 +01:00
Joost Lekkerkerker
e55162812d Add integration_type hub to blink (#158862) 2025-12-13 08:22:22 +01:00
Joost Lekkerkerker
aa6ccaa024 Add integration_type device to blebox (#158860) 2025-12-13 08:21:25 +01:00
Joost Lekkerkerker
e1b009a6de Add integration_type device to balboa (#158859) 2025-12-13 08:20:12 +01:00
Joost Lekkerkerker
91ddc525b0 Add integration_type service to azure_event_hub (#158857) 2025-12-13 08:18:56 +01:00
Joost Lekkerkerker
d7d7954ac2 Add integration_type service to azure_devops (#158856) 2025-12-13 08:18:35 +01:00
Joost Lekkerkerker
e87c260df7 Add integration_type service to azure_data_explorer (#158855) 2025-12-13 08:18:13 +01:00
Joost Lekkerkerker
5185c6cd68 Add integration_type hub to arve (#158848) 2025-12-13 08:17:37 +01:00
Joost Lekkerkerker
7599c918e2 Add integration_type hub to august (#158851) 2025-12-12 23:00:06 +01:00
Joost Lekkerkerker
fa7e22ec91 Add integration_type device to arcam_fmj (#158846) 2025-12-12 22:59:47 +01:00
Joost Lekkerkerker
606519e51b Add integration_type device to baf (#158858) 2025-12-12 22:59:28 +01:00
Joost Lekkerkerker
8e39e010f7 Add integration_type device to bluemaestro (#158864) 2025-12-12 22:59:13 +01:00
Joost Lekkerkerker
dc01cf49a0 Add integration_type hub to bond (#158867) 2025-12-12 22:58:57 +01:00
Joost Lekkerkerker
1f3ad382f1 Set Denon AVR integration type to device (#158815) 2025-12-12 20:33:56 +01:00
Joost Lekkerkerker
2595c7dcb2 Set Actron Air integration type to hub (#158816) 2025-12-12 20:33:25 +01:00
Kamil Breguła
d445b320de Accept URLs in WLED Host input (#157793)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2025-12-12 18:55:03 +01:00
epenet
7b6df1a8a0 Cleanup deprecated typing helpers (#158806) 2025-12-12 17:04:41 +01:00
Manu
2a151dcd19 Add tests for discovery to Xbox integration (#158808) 2025-12-12 17:02:32 +01:00
epenet
adbab150af Move blue_current services to separate module (#158389) 2025-12-12 16:50:34 +01:00
Allen Porter
d20edf7928 Improve Roborock exception logging behavior for Zeo/Dyad devices (#158465)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-12 16:43:45 +01:00
Ludovic BOUÉ
7d6d37fe76 Fix Matter Door Lock Operating Mode select entity (#158468) 2025-12-12 16:30:48 +01:00
Ludovic BOUÉ
228e0453a7 Add Matter Thermostat remote sensing status (#157650) 2025-12-12 16:26:27 +01:00
Raphael Hehl
1da31c0530 Move icons to icons.json for unifiprotect (#158800)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2025-12-12 16:23:11 +01:00
Joost Lekkerkerker
41ad15e577 Bump pySmartThings to 3.5.1 (#158795) 2025-12-12 15:45:08 +01:00
Markus Jacobsen
421af881fe Add video source reporting to Bang & Olufsen (#158675) 2025-12-12 15:40:12 +01:00
Klaas Schoute
715a484f7e Add AutarcoSensorBase class for Autarco sensors (#158691)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-12 15:39:18 +01:00
Samuel Xiao
0a789f51b8 Switchbot Cloud: Fixed binary sensors didn't update automatically (#158434)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-12 15:39:08 +01:00
epenet
fa25d45123 Remove incorrect bring test (#158797) 2025-12-12 15:32:27 +01:00
johanzander
6d255b2521 Add state_class to Growatt power and energy sensors (#158705)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 15:27:47 +01:00
peteS-UK
5ffb39f064 Trap for missing UUID in config_flow for Squeezebox (#158721)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-12 15:15:04 +01:00
Zoltán Farkasdi
d642109436 Netatmo NOCamera on/off fix (#158741) 2025-12-12 14:57:57 +01:00
Heindrich Paul
10f6d8d14f Add diagnostics support for Nederlandse Spoorwegen integration (#158722)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-12 14:36:35 +01:00
Thomas55555
a94678cb06 Bump google air quality api to 2.0.2 (#158742) 2025-12-12 14:35:07 +01:00
Manu
0d8d466003 Increase Xbox update interval to 15 seconds and refactor title data handling (#158780) 2025-12-12 14:04:25 +01:00
Marc Mueller
8ddf3e1734 Update pytest warnings filter (#158790) 2025-12-12 13:54:00 +01:00
cdutr
d88047a750 Migrate Blink component to use hardware_id instead of device_id (#158765) 2025-12-12 13:49:19 +01:00
Jordan Harvey
61c7ac81d6 Bump pynintendoparental to 2.1.1 (#158779) 2025-12-12 13:45:48 +01:00
Josef Zweck
bbe07bddb0 Bump pylamarzocco to 2.2.4 (#158774) 2025-12-12 13:45:14 +01:00
Denis Shulyaka
a3afc2beb1 Bump openai to 2.11.0 (#158785) 2025-12-12 13:40:43 +01:00
epenet
374cd93d3d Replace Tuya remap methods with helper class (#158718) 2025-12-12 13:37:29 +01:00
Maciej Bieniek
6e99411084 Add get_kvs_value and set_kvs_value actions for Shelly RPC devices (#157349) 2025-12-12 13:15:25 +01:00
dependabot[bot]
41d5415c86 Bump actions/cache from 4.3.0 to 5.0.0 (#158771) 2025-12-12 10:39:58 +01:00
Allen Porter
052d56f358 Bump ical to 12.1.1 (#158770) 2025-12-12 08:34:22 +01:00
Abílio Costa
0a676b5812 Remove alarm panel test from text tests (#158743) 2025-12-12 02:18:40 +01:00
Michael
1f4cf67daa Add turned off and turned on triggers to switch platform (#158688)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-11 22:02:22 +00:00
Maikel Punie
bb4ec229ce Add Velbus VLP file loading (#154883) 2025-12-11 22:53:01 +01:00
ndrwrbgs
ff62b460d5 Update advanced_options display text for MQTT (#158728) 2025-12-11 22:16:35 +01:00
165 changed files with 4530 additions and 808 deletions

View File

@@ -263,7 +263,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: &actions-cache actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: &actions-cache actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0
with:
path: venv
key: &key-pre-commit-venv >-
@@ -304,7 +304,7 @@ jobs:
- &cache-restore-pre-commit-venv
name: Restore base Python virtual environment
id: cache-venv
uses: &actions-cache-restore actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: &actions-cache-restore actions/cache/restore@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0
with:
path: venv
fail-on-cache-miss: true
@@ -511,7 +511,7 @@ jobs:
fi
- name: Save apt cache
if: steps.cache-apt-check.outputs.cache-hit != 'true'
uses: &actions-cache-save actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: &actions-cache-save actions/cache/save@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0
with:
path: *path-apt-cache
key: *key-apt-cache

4
CODEOWNERS generated
View File

@@ -220,8 +220,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
/homeassistant/components/blebox/ @bbx-a @swistakm
/tests/components/blebox/ @bbx-a @swistakm
/homeassistant/components/blink/ @fronzbot @mkmer
/tests/components/blink/ @fronzbot @mkmer
/homeassistant/components/blink/ @fronzbot
/tests/components/blink/ @fronzbot
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
/tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
/homeassistant/components/bluemaestro/ @bdraco

View File

@@ -10,6 +10,7 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/actron_air",
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["actron-neo-api==0.1.87"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@Bre77"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/advantage_air",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["advantage_air"],
"requirements": ["advantage-air==0.4.4"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@Noltari"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aemet",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["aemet_opendata"],
"requirements": ["AEMET-OpenData==0.6.4"]

View File

@@ -4,6 +4,7 @@
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aftership",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["pyaftership==21.11.0"]
}

View File

@@ -4,6 +4,7 @@
"codeowners": ["@ispysoftware"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/agent_dvr",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["agent"],
"requirements": ["agent-py==0.0.24"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@asymworks"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airnow",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["pyairnow"],
"requirements": ["pyairnow==1.3.1"]

View File

@@ -17,6 +17,7 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/airthings",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["airthings"],
"requirements": ["airthings-cloud==0.2.0"]

View File

@@ -27,6 +27,7 @@
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["airthings-ble==1.2.0"]
}

View File

@@ -4,6 +4,7 @@
"codeowners": ["@samsinnamon"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airtouch4",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["airtouch4pyapi"],
"requirements": ["airtouch4pyapi==1.0.5"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@danzel"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["airtouch5py"],
"requirements": ["airtouch5py==0.3.0"]

View File

@@ -9,6 +9,7 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/airzone",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==1.0.4"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@Noltari"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.7.2"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@madpilot"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["amberelectric"],
"requirements": ["amberelectric==2.0.12"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@engrbm87"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["pydroid-ipcam==3.0.0"]
}

View File

@@ -4,6 +4,7 @@
"codeowners": ["@Lash-L"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/anova",
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["anova_wifi"],
"requirements": ["anova-wifi==0.17.0"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@hyralex"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/anthemav",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["anthemav"],
"requirements": ["anthemav==1.4.1"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@bdr99"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aosmith",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": ["py-aosmith==1.0.15"]
}

View File

@@ -4,6 +4,7 @@
"codeowners": ["@elupus"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["arcam"],
"requirements": ["arcam-fmj==1.8.2"],

View File

@@ -4,6 +4,7 @@
"codeowners": ["@ikalnyi"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/arve",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": ["asyncarve==0.1.1"]
}

View File

@@ -4,6 +4,7 @@
"codeowners": ["@milanmeu"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aioaseko"],
"requirements": ["aioaseko==1.0.0"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@MatsNL"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/atag",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pyatag"],
"requirements": ["pyatag==0.3.5.3"]

View File

@@ -27,6 +27,7 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/august",
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.2"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@djtimca"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aurora",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["auroranoaa"],
"requirements": ["auroranoaa==0.0.5"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@nickw444", "@Bre77"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aussie_broadband",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["aussiebb"],
"requirements": ["pyaussiebb==0.1.5"]

View File

@@ -6,10 +6,7 @@ rules:
This integration does not provide additional actions.
appropriate-polling: done
brands: done
common-modules:
status: todo
comment: |
The entity.py file is not used in this integration.
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done

View File

@@ -204,13 +204,25 @@ async def async_setup_entry(
async_add_entities(entities)
class AutarcoBatterySensorEntity(
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
):
class AutarcoSensorBase(CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity):
"""Base class for Autarco sensors."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AutarcoDataUpdateCoordinator,
description: SensorEntityDescription,
) -> None:
"""Initialize Autarco sensor base."""
super().__init__(coordinator)
self.entity_description = description
class AutarcoBatterySensorEntity(AutarcoSensorBase):
"""Defines an Autarco battery sensor."""
entity_description: AutarcoBatterySensorEntityDescription
_attr_has_entity_name = True
def __init__(
self,
@@ -218,10 +230,8 @@ class AutarcoBatterySensorEntity(
coordinator: AutarcoDataUpdateCoordinator,
description: AutarcoBatterySensorEntityDescription,
) -> None:
"""Initialize Autarco sensor."""
super().__init__(coordinator)
self.entity_description = description
"""Initialize Autarco battery sensor."""
super().__init__(coordinator, description)
self._attr_unique_id = (
f"{coordinator.account_site.site_id}_battery_{description.key}"
)
@@ -239,13 +249,10 @@ class AutarcoBatterySensorEntity(
return self.entity_description.value_fn(self.coordinator.data.battery)
class AutarcoSolarSensorEntity(
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
):
class AutarcoSolarSensorEntity(AutarcoSensorBase):
"""Defines an Autarco solar sensor."""
entity_description: AutarcoSolarSensorEntityDescription
_attr_has_entity_name = True
def __init__(
self,
@@ -253,10 +260,8 @@ class AutarcoSolarSensorEntity(
coordinator: AutarcoDataUpdateCoordinator,
description: AutarcoSolarSensorEntityDescription,
) -> None:
"""Initialize Autarco sensor."""
super().__init__(coordinator)
self.entity_description = description
"""Initialize Autarco solar sensor."""
super().__init__(coordinator, description)
self._attr_unique_id = (
f"{coordinator.account_site.site_id}_solar_{description.key}"
)
@@ -273,13 +278,10 @@ class AutarcoSolarSensorEntity(
return self.entity_description.value_fn(self.coordinator.data.solar)
class AutarcoInverterSensorEntity(
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
):
class AutarcoInverterSensorEntity(AutarcoSensorBase):
"""Defines an Autarco inverter sensor."""
entity_description: AutarcoInverterSensorEntityDescription
_attr_has_entity_name = True
def __init__(
self,
@@ -288,10 +290,8 @@ class AutarcoInverterSensorEntity(
description: AutarcoInverterSensorEntityDescription,
serial_number: str,
) -> None:
"""Initialize Autarco sensor."""
super().__init__(coordinator)
self.entity_description = description
"""Initialize Autarco inverter sensor."""
super().__init__(coordinator, description)
self._serial_number = serial_number
self._attr_unique_id = f"{serial_number}_{description.key}"
self._attr_device_info = DeviceInfo(

View File

@@ -131,6 +131,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"lawn_mower",
"light",
"media_player",
"switch",
"text",
"vacuum",
}

View File

@@ -4,6 +4,7 @@
"codeowners": ["@kaareseras"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/azure_data_explorer",
"integration_type": "service",
"iot_class": "cloud_push",
"loggers": ["azure"],
"requirements": ["azure-kusto-ingest==4.5.1", "azure-kusto-data[aio]==4.5.1"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@timmo001"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/azure_devops",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["aioazuredevops"],
"requirements": ["aioazuredevops==2.2.2"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@eavanvalkenburg"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/azure_event_hub",
"integration_type": "service",
"iot_class": "cloud_push",
"loggers": ["azure"],
"requirements": ["azure-eventhub==5.11.1"],

View File

@@ -4,6 +4,7 @@
"codeowners": ["@bdraco", "@jfroy"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/baf",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["aiobafi6==0.9.0"],
"zeroconf": [

View File

@@ -12,6 +12,7 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/balboa",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pybalboa"],
"requirements": ["pybalboa==1.1.3"]

View File

@@ -22,6 +22,7 @@ class BeoSource:
NET_RADIO: Final[Source] = Source(name="B&O Radio", id="netRadio")
SPDIF: Final[Source] = Source(name="Optical", id="spdif")
TIDAL: Final[Source] = Source(name="Tidal", id="tidal")
TV: Final[Source] = Source(name="TV", id="tv")
UNKNOWN: Final[Source] = Source(name="Unknown Source", id="unknown")
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
@@ -55,12 +56,13 @@ BEO_REPEAT_TO_HA: dict[str, RepeatMode] = {
class BeoMediaType(StrEnum):
"""Bang & Olufsen specific media types."""
FAVOURITE = "favourite"
DEEZER = "deezer"
FAVOURITE = "favourite"
OVERLAY_TTS = "overlay_tts"
RADIO = "radio"
TIDAL = "tidal"
TTS = "provider"
OVERLAY_TTS = "overlay_tts"
TV = "tv"
class BeoModel(StrEnum):

View File

@@ -218,6 +218,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
self._sources: dict[str, str] = {}
self._state: str = MediaPlayerState.IDLE
self._video_sources: dict[str, str] = {}
self._video_source_id_map: dict[str, str] = {}
self._sound_modes: dict[str, int] = {}
# Beolink compatible sources
@@ -355,6 +356,9 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
and menu_item.label != "TV"
):
self._video_sources[key] = menu_item.label
self._video_source_id_map[
menu_item.content.content_uri.removeprefix("tv://")
] = menu_item.label
# Combine the source dicts
self._sources = self._audio_sources | self._video_sources
@@ -627,10 +631,11 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
def media_content_type(self) -> MediaType | str | None:
"""Return the current media type."""
content_type = {
BeoSource.URI_STREAMER.id: MediaType.URL,
BeoSource.DEEZER.id: BeoMediaType.DEEZER,
BeoSource.TIDAL.id: BeoMediaType.TIDAL,
BeoSource.NET_RADIO.id: BeoMediaType.RADIO,
BeoSource.TIDAL.id: BeoMediaType.TIDAL,
BeoSource.TV.id: BeoMediaType.TV,
BeoSource.URI_STREAMER.id: MediaType.URL,
}
# Hard to determine content type.
if self._source_change.id in content_type:
@@ -690,7 +695,11 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
@property
def source(self) -> str | None:
"""Return the current audio source."""
"""Return the current audio/video source."""
# Associate TV content ID with a video source
if self.media_content_id in self._video_source_id_map:
return self._video_source_id_map[self.media_content_id]
return self._source_change.name
@property

View File

@@ -4,6 +4,7 @@
"codeowners": ["@bbx-a", "@swistakm"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/blebox",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["blebox_uniapi"],
"requirements": ["blebox-uniapi==2.5.0"],

View File

@@ -64,6 +64,12 @@ async def async_migrate_entry(hass: HomeAssistant, entry: BlinkConfigEntry) -> b
if entry.version == 2:
await _reauth_flow_wrapper(hass, entry, data)
return False
if entry.version == 3:
# Migrate device_id to hardware_id for blinkpy 0.25.x OAuth2 compatibility
if "device_id" in data:
data["hardware_id"] = data.pop("device_id")
hass.config_entries.async_update_entry(entry, data=data, version=4)
return True
return True

View File

@@ -21,7 +21,7 @@ from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DEVICE_ID, DOMAIN
from .const import DOMAIN, HARDWARE_ID
_LOGGER = logging.getLogger(__name__)
@@ -43,7 +43,7 @@ async def _send_blink_2fa_pin(blink: Blink, pin: str | None) -> bool:
class BlinkConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a Blink config flow."""
VERSION = 3
VERSION = 4
def __init__(self) -> None:
"""Initialize the blink flow."""
@@ -53,7 +53,7 @@ class BlinkConfigFlow(ConfigFlow, domain=DOMAIN):
async def _handle_user_input(self, user_input: dict[str, Any]):
"""Handle user input."""
self.auth = Auth(
{**user_input, "device_id": DEVICE_ID},
{**user_input, "hardware_id": HARDWARE_ID},
no_prompt=True,
session=async_get_clientsession(self.hass),
)

View File

@@ -3,7 +3,7 @@
from homeassistant.const import Platform
DOMAIN = "blink"
DEVICE_ID = "Home Assistant"
HARDWARE_ID = "Home Assistant"
CONF_MIGRATE = "migrate"
CONF_CAMERA = "camera"

View File

@@ -1,7 +1,7 @@
{
"domain": "blink",
"name": "Blink",
"codeowners": ["@fronzbot", "@mkmer"],
"codeowners": ["@fronzbot"],
"config_flow": true,
"dhcp": [
{
@@ -18,6 +18,7 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/blink",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["blinkpy"],
"requirements": ["blinkpy==0.25.1"]

View File

@@ -13,32 +13,25 @@ from bluecurrent_api.exceptions import (
RequestLimitReached,
WebsocketError,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_API_TOKEN, CONF_DEVICE_ID, Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
ServiceValidationError,
)
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from .const import (
BCU_APP,
CHARGEPOINT_SETTINGS,
CHARGEPOINT_STATUS,
CHARGING_CARD_ID,
DOMAIN,
EVSE_ID,
LOGGER,
PLUG_AND_CHARGE,
SERVICE_START_CHARGE_SESSION,
VALUE,
)
from .services import async_setup_services
type BlueCurrentConfigEntry = ConfigEntry[Connector]
@@ -54,13 +47,12 @@ VALUE_TYPES = [CHARGEPOINT_STATUS, CHARGEPOINT_SETTINGS]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
SERVICE_START_CHARGE_SESSION_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE_ID): cv.string,
# When no charging card is provided, use no charging card (BCU_APP = no charging card).
vol.Optional(CHARGING_CARD_ID, default=BCU_APP): cv.string,
}
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Blue Current."""
async_setup_services(hass)
return True
async def async_setup_entry(
@@ -88,66 +80,6 @@ async def async_setup_entry(
return True
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Blue Current."""
async def start_charge_session(service_call: ServiceCall) -> None:
"""Start a charge session with the provided device and charge card ID."""
# When no charge card is provided, use the default charge card set in the config flow.
charging_card_id = service_call.data[CHARGING_CARD_ID]
device_id = service_call.data[CONF_DEVICE_ID]
# Get the device based on the given device ID.
device = dr.async_get(hass).devices.get(device_id)
if device is None:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="invalid_device_id"
)
blue_current_config_entry: ConfigEntry | None = None
for config_entry_id in device.config_entries:
config_entry = hass.config_entries.async_get_entry(config_entry_id)
if not config_entry or config_entry.domain != DOMAIN:
# Not the blue_current config entry.
continue
if config_entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="config_entry_not_loaded"
)
blue_current_config_entry = config_entry
break
if not blue_current_config_entry:
# The device is not connected to a valid blue_current config entry.
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="no_config_entry"
)
connector = blue_current_config_entry.runtime_data
# Get the evse_id from the identifier of the device.
evse_id = next(
identifier[1]
for identifier in device.identifiers
if identifier[0] == DOMAIN
)
await connector.client.start_session(evse_id, charging_card_id)
hass.services.async_register(
DOMAIN,
SERVICE_START_CHARGE_SESSION,
start_charge_session,
SERVICE_START_CHARGE_SESSION_SCHEMA,
)
return True
async def async_unload_entry(
hass: HomeAssistant, config_entry: BlueCurrentConfigEntry
) -> bool:

View File

@@ -4,6 +4,7 @@
"codeowners": ["@gleeuwen", "@NickKoepr", "@jtodorova23"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/blue_current",
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["bluecurrent_api"],
"requirements": ["bluecurrent-api==1.3.2"]

View File

@@ -0,0 +1,79 @@
"""The Blue Current integration."""
from __future__ import annotations
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_DEVICE_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
from .const import BCU_APP, CHARGING_CARD_ID, DOMAIN, SERVICE_START_CHARGE_SESSION
SERVICE_START_CHARGE_SESSION_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE_ID): cv.string,
# When no charging card is provided, use no charging card (BCU_APP = no charging card).
vol.Optional(CHARGING_CARD_ID, default=BCU_APP): cv.string,
}
)
async def start_charge_session(service_call: ServiceCall) -> None:
"""Start a charge session with the provided device and charge card ID."""
# When no charge card is provided, use the default charge card set in the config flow.
charging_card_id = service_call.data[CHARGING_CARD_ID]
device_id = service_call.data[CONF_DEVICE_ID]
# Get the device based on the given device ID.
device = dr.async_get(service_call.hass).devices.get(device_id)
if device is None:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="invalid_device_id"
)
blue_current_config_entry: ConfigEntry | None = None
for config_entry_id in device.config_entries:
config_entry = service_call.hass.config_entries.async_get_entry(config_entry_id)
if not config_entry or config_entry.domain != DOMAIN:
# Not the blue_current config entry.
continue
if config_entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="config_entry_not_loaded"
)
blue_current_config_entry = config_entry
break
if not blue_current_config_entry:
# The device is not connected to a valid blue_current config entry.
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="no_config_entry"
)
connector = blue_current_config_entry.runtime_data
# Get the evse_id from the identifier of the device.
evse_id = next(
identifier[1] for identifier in device.identifiers if identifier[0] == DOMAIN
)
await connector.client.start_session(evse_id, charging_card_id)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register the services."""
hass.services.async_register(
DOMAIN,
SERVICE_START_CHARGE_SESSION,
start_charge_session,
SERVICE_START_CHARGE_SESSION_SCHEMA,
)

View File

@@ -11,6 +11,7 @@
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bluemaestro",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["bluemaestro-ble==0.4.1"]
}

View File

@@ -5,6 +5,7 @@
"codeowners": ["@thrawnarn", "@LouisChrist"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bluesound",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["pyblu==2.0.5"],
"zeroconf": [

View File

@@ -4,6 +4,7 @@
"codeowners": ["@gerard33", "@rikroe"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["bimmer_connected"],
"requirements": ["bimmer-connected[china]==0.17.3"]

View File

@@ -14,6 +14,7 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/bond",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["bond_async"],
"requirements": ["bond-async==0.2.1"],

View File

@@ -5,6 +5,7 @@
"codeowners": ["@tschamm"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bosch_shc",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["boschshcpy"],
"requirements": ["boschshcpy==0.2.107"],

View File

@@ -4,6 +4,7 @@
"codeowners": ["@gjohansson-ST"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/brottsplatskartan",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["brottsplatskartan"],
"requirements": ["brottsplatskartan==1.0.5"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@eavanvalkenburg"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/brunt",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["brunt"],
"requirements": ["brunt==1.2.0"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@mjj4791", "@ties", "@Robbie1221"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/buienradar",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["buienradar", "vincenty"],
"requirements": ["buienradar==1.0.6"]

View File

@@ -4,6 +4,7 @@
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/caldav",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["caldav", "vobject"],
"requirements": ["caldav==2.1.0", "icalendar==6.3.1", "vobject==0.9.9"]

View File

@@ -5,6 +5,7 @@
"config_flow": true,
"dependencies": ["ffmpeg"],
"documentation": "https://www.home-assistant.io/integrations/canary",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["canary"],
"requirements": ["py-canary==0.5.4"],

View File

@@ -4,6 +4,7 @@
"codeowners": ["@ocalvo"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ccm15",
"integration_type": "hub",
"iot_class": "local_polling",
"requirements": ["py_ccm15==0.1.2"]
}

View File

@@ -4,5 +4,6 @@
"codeowners": ["@jjlawren"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cert_expiry",
"integration_type": "service",
"iot_class": "cloud_polling"
}

View File

@@ -4,6 +4,7 @@
"codeowners": ["@ludeeus", "@ctalkington"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cloudflare",
"integration_type": "service",
"iot_class": "cloud_push",
"loggers": ["pycfdns"],
"requirements": ["pycfdns==3.0.0"],

View File

@@ -4,6 +4,7 @@
"codeowners": ["@tombrien"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/coinbase",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["coinbase"],
"requirements": ["coinbase-advanced-py==1.2.2"]

View File

@@ -4,6 +4,7 @@
"codeowners": ["@lawtancool"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/control4",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pyControl4"],
"requirements": ["pyControl4==1.5.0"],

View File

@@ -4,6 +4,7 @@
"codeowners": ["@ol-iver", "@starkillerOG"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/denonavr",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["denonavr"],
"requirements": ["denonavr==1.2.0"],

View File

@@ -8,5 +8,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==11.1.0"]
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==12.1.1"]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["google_air_quality_api"],
"quality_scale": "bronze",
"requirements": ["google_air_quality_api==2.0.0"]
"requirements": ["google_air_quality_api==2.0.2"]
}

View File

@@ -88,16 +88,16 @@
"1b_good_air_quality": "1B - Good air quality",
"2_cyan": "2 - Cyan",
"2_light_green": "2 - Light green",
"2_orange": "4 - Orange",
"2_red": "5 - Red",
"2_yellow": "3 - Yellow",
"2a_acceptable_air_quality": "2A - Acceptable air quality",
"2b_acceptable_air_quality": "2B - Acceptable air quality",
"3_green": "3 - Green",
"3_yellow": "3 - Yellow",
"3a_aggravated_air_quality": "3A - Aggravated air quality",
"3b_bad_air_quality": "3B - Bad air quality",
"4_orange": "4 - Orange",
"4_yellow_watch": "4 - Yellow/Watch",
"5_orange_alert": "5 - Orange/Alert",
"5_red": "5 - Red",
"6_red_alert": "6 - Red/Alert+",
"10_33": "10-33% of guideline",
"33_66": "33-66% of guideline",

View File

@@ -27,6 +27,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="eBatChargeToday",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_battery_charge_lifetime",
@@ -42,6 +43,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="eBatDisChargeToday",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_battery_discharge_lifetime",
@@ -57,6 +59,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="epvToday",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_solar_generation_lifetime",
@@ -72,6 +75,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pDischarge1",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_battery_voltage",
@@ -101,6 +105,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="elocalLoadToday",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_load_consumption_lifetime",
@@ -116,6 +121,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="etoGridToday",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_export_to_grid_lifetime",
@@ -132,6 +138,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="chargePower",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_load_consumption",
@@ -139,6 +146,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pLocalLoad",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_wattage_pv_1",
@@ -146,6 +154,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pPv1",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_wattage_pv_2",
@@ -153,6 +162,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pPv2",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_wattage_pv_all",
@@ -160,6 +170,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_export_to_grid",
@@ -167,6 +178,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pactogrid",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_import_from_grid",
@@ -174,6 +186,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pactouser",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_battery_discharge_kw",
@@ -181,6 +194,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pdisCharge1",
native_unit_of_measurement=UnitOfPower.KILO_WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="mix_grid_voltage",
@@ -196,6 +210,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="eCharge",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_load_consumption_solar_today",
@@ -203,6 +218,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="eChargeToday",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_self_consumption_today",
@@ -210,6 +226,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="eChargeToday1",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_load_consumption_battery_today",
@@ -217,6 +234,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="echarge1",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
GrowattSensorEntityDescription(
key="mix_import_from_grid_today",
@@ -224,6 +242,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="etouser",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
# This sensor is manually created using the most recent X-Axis value from the chartData
GrowattSensorEntityDescription(

View File

@@ -79,6 +79,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv1",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -122,6 +123,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv2",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -165,6 +167,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv3",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -208,6 +211,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv4",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -234,6 +238,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -258,6 +263,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pac",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -323,6 +329,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="bdc1DischargePower",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="tlx_battery_1_discharge_total",
@@ -339,6 +346,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="bdc2DischargePower",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="tlx_battery_2_discharge_total",
@@ -372,6 +380,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="bdc1ChargePower",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="tlx_battery_1_charge_total",
@@ -388,6 +397,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="bdc2ChargePower",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
GrowattSensorEntityDescription(
key="tlx_battery_2_charge_total",
@@ -445,6 +455,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pacToLocalLoad",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -453,6 +464,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pacToUserTotal",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -461,6 +473,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pacToGridTotal",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -545,6 +558,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="psystem",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -553,6 +567,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pself",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
)

View File

@@ -50,5 +50,6 @@ TOTAL_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="nominalPower",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
)

View File

@@ -37,5 +37,5 @@
"iot_class": "cloud_push",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==2.2.3"]
"requirements": ["pylamarzocco==2.2.4"]
}

View File

@@ -1,6 +1,5 @@
"""Support for LCN climate control."""
import asyncio
from collections.abc import Iterable
from datetime import timedelta
from functools import partial
@@ -172,14 +171,14 @@ class LcnClimate(LcnEntity, ClimateEntity):
async def async_update(self) -> None:
"""Update the state of the entity."""
self._attr_available = any(
await asyncio.gather(
self.device_connection.request_status_variable(
[
await self.device_connection.request_status_variable(
self.variable, SCAN_INTERVAL.seconds
),
self.device_connection.request_status_variable(
await self.device_connection.request_status_variable(
self.setpoint, SCAN_INTERVAL.seconds
),
)
]
)
def input_received(self, input_obj: InputType) -> None:

View File

@@ -1,6 +1,5 @@
"""Support for LCN covers."""
import asyncio
from collections.abc import Coroutine, Iterable
from datetime import timedelta
from functools import partial
@@ -134,14 +133,14 @@ class LcnOutputsCover(LcnEntity, CoverEntity):
"""Update the state of the entity."""
if not self.device_connection.is_group:
self._attr_available = any(
await asyncio.gather(
self.device_connection.request_status_output(
[
await self.device_connection.request_status_output(
pypck.lcn_defs.OutputPort["OUTPUTUP"], SCAN_INTERVAL.seconds
),
self.device_connection.request_status_output(
await self.device_connection.request_status_output(
pypck.lcn_defs.OutputPort["OUTPUTDOWN"], SCAN_INTERVAL.seconds
),
)
]
)
def input_received(self, input_obj: InputType) -> None:
@@ -274,7 +273,7 @@ class LcnRelayCover(LcnEntity, CoverEntity):
self.motor, self.positioning_mode, SCAN_INTERVAL.seconds
)
)
self._attr_available = any(await asyncio.gather(*coros))
self._attr_available = any([await coro for coro in coros])
def input_received(self, input_obj: InputType) -> None:
"""Set cover states when LCN input object (command) is received."""

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling",
"loggers": ["ical"],
"requirements": ["ical==11.1.0"]
"requirements": ["ical==12.1.1"]
}

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_todo",
"iot_class": "local_polling",
"requirements": ["ical==11.1.0"]
"requirements": ["ical==12.1.1"]
}

View File

@@ -499,4 +499,53 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterBinarySensor,
required_attributes=(clusters.WindowCovering.Attributes.ConfigStatus,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="ThermostatRemoteSensing_LocalTemperature",
translation_key="thermostat_remote_sensing_local_temperature",
entity_category=EntityCategory.DIAGNOSTIC,
# LocalTemperature bit from RemoteSensing attribute
device_to_ha=lambda x: bool(
x
& clusters.Thermostat.Bitmaps.RemoteSensingBitmap.kLocalTemperature # Calculated Local Temperature is derived from a remote node
),
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.Thermostat.Attributes.RemoteSensing,),
allow_multi=True,
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="ThermostatRemoteSensing_OutdoorTemperature",
translation_key="thermostat_remote_sensing_outdoor_temperature",
entity_category=EntityCategory.DIAGNOSTIC,
# OutdoorTemperature bit from RemoteSensing attribute
device_to_ha=lambda x: bool(
x
& clusters.Thermostat.Bitmaps.RemoteSensingBitmap.kOutdoorTemperature # OutdoorTemperature is derived from a remote node
),
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.Thermostat.Attributes.RemoteSensing,),
allow_multi=True,
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="ThermostatRemoteSensing_Occupancy",
translation_key="thermostat_remote_sensing_occupancy",
entity_category=EntityCategory.DIAGNOSTIC,
# Occupancy bit from RemoteSensing attribute
device_to_ha=lambda x: bool(
x
& clusters.Thermostat.Bitmaps.RemoteSensingBitmap.kOccupancy # Occupancy is derived from a remote node
),
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.Thermostat.Attributes.RemoteSensing,),
featuremap_contains=clusters.Thermostat.Bitmaps.Feature.kOccupancy,
allow_multi=True,
),
]

View File

@@ -183,6 +183,48 @@ class MatterModeSelectEntity(MatterAttributeSelectEntity):
self._attr_name = desc
class MatterDoorLockOperatingModeSelectEntity(MatterAttributeSelectEntity):
"""Representation of a Door Lock Operating Mode select entity.
This entity dynamically filters available operating modes based on the device's
`SupportedOperatingModes` bitmap attribute. In this bitmap, bit=0 indicates a
supported mode and bit=1 indicates unsupported (inverted from typical bitmap conventions).
If the bitmap is unavailable, only mandatory modes are included. The mapping from
bitmap bits to operating mode values is defined by the Matter specification.
"""
entity_description: MatterMapSelectEntityDescription
@callback
def _update_from_device(self) -> None:
"""Update from device."""
# Get the bitmap of supported operating modes
supported_modes_bitmap = self.get_matter_attribute_value(
self.entity_description.list_attribute
)
# Convert bitmap to list of supported mode values
# NOTE: The Matter spec inverts the usual meaning: bit=0 means supported,
# bit=1 means not supported, undefined bits must be 1. Mandatory modes are
# bits 0 (Normal) and 3 (NoRemoteLockUnlock).
num_mode_bits = supported_modes_bitmap.bit_length()
supported_mode_values = [
bit_position
for bit_position in range(num_mode_bits)
if not supported_modes_bitmap & (1 << bit_position)
]
# Map supported mode values to their string representations
self._attr_options = [
mapped_value
for mode_value in supported_mode_values
if (mapped_value := self.entity_description.device_to_ha(mode_value))
]
# Use base implementation to set the current option
super()._update_from_device()
class MatterListSelectEntity(MatterEntity, SelectEntity):
"""Representation of a select entity from Matter list and selected item Cluster attribute(s)."""
@@ -594,15 +636,18 @@ DISCOVERY_SCHEMAS = [
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
entity_description=MatterMapSelectEntityDescription(
key="DoorLockOperatingMode",
entity_category=EntityCategory.CONFIG,
translation_key="door_lock_operating_mode",
options=list(DOOR_LOCK_OPERATING_MODE_MAP.values()),
list_attribute=clusters.DoorLock.Attributes.SupportedOperatingModes,
device_to_ha=DOOR_LOCK_OPERATING_MODE_MAP.get,
ha_to_device=DOOR_LOCK_OPERATING_MODE_MAP_REVERSE.get,
),
entity_class=MatterAttributeSelectEntity,
required_attributes=(clusters.DoorLock.Attributes.OperatingMode,),
entity_class=MatterDoorLockOperatingModeSelectEntity,
required_attributes=(
clusters.DoorLock.Attributes.OperatingMode,
clusters.DoorLock.Attributes.SupportedOperatingModes,
),
),
]

View File

@@ -89,6 +89,15 @@
"test_in_progress": {
"name": "Test in progress"
},
"thermostat_remote_sensing_local_temperature": {
"name": "Local temperature remote sensing"
},
"thermostat_remote_sensing_occupancy": {
"name": "Occupancy remote sensing"
},
"thermostat_remote_sensing_outdoor_temperature": {
"name": "Outdoor temperature remote sensing"
},
"valve_fault_blocked": {
"name": "Valve blocked"
},

View File

@@ -46,7 +46,7 @@
"ws_path": "WebSocket path"
},
"data_description": {
"advanced_options": "Enable and select **Next** to set advanced options.",
"advanced_options": "Enable and select **Submit** to set advanced options.",
"broker": "The hostname or IP address of your MQTT broker.",
"certificate": "The custom CA certificate file to validate your MQTT brokers certificate.",
"client_cert": "The client certificate to authenticate against your MQTT broker.",

View File

@@ -0,0 +1,122 @@
"""Diagnostics support for Nederlandse Spoorwegen."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry
from .const import DOMAIN
from .coordinator import NSConfigEntry
TO_REDACT = [
CONF_API_KEY,
]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: NSConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinators_data = {}
# Collect data from all coordinators
for subentry_id, coordinator in entry.runtime_data.items():
coordinators_data[subentry_id] = {
"coordinator_info": {
"name": coordinator.name,
"departure": coordinator.departure,
"destination": coordinator.destination,
"via": coordinator.via,
"departure_time": coordinator.departure_time,
},
"route_data": {
"trips_count": len(coordinator.data.trips) if coordinator.data else 0,
"has_first_trip": coordinator.data.first_trip is not None
if coordinator.data
else False,
"has_next_trip": coordinator.data.next_trip is not None
if coordinator.data
else False,
}
if coordinator.data
else None,
}
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"coordinators": coordinators_data,
}
async def async_get_device_diagnostics(
hass: HomeAssistant, entry: NSConfigEntry, device: DeviceEntry
) -> dict[str, Any]:
"""Return diagnostics for a route."""
# Find the coordinator for this device
coordinator = None
subentry_id = None
# Each device has an identifier (DOMAIN, subentry_id)
for identifier in device.identifiers:
if identifier[0] == DOMAIN:
subentry_id = identifier[1]
coordinator = entry.runtime_data.get(subentry_id)
break
# Collect detailed diagnostics for this specific route
device_data = {
"device_info": {
"subentry_id": subentry_id,
"device_name": device.name,
"manufacturer": device.manufacturer,
"model": device.model,
},
"coordinator_info": {
"name": coordinator.name,
"departure": coordinator.departure,
"destination": coordinator.destination,
"via": coordinator.via,
"departure_time": coordinator.departure_time,
}
if coordinator
else None,
}
# Add detailed trip data if available
if coordinator and coordinator.data:
device_data["trip_details"] = {
"trips_count": len(coordinator.data.trips),
"has_first_trip": coordinator.data.first_trip is not None,
"has_next_trip": coordinator.data.next_trip is not None,
}
# Add first trip details if available
if coordinator.data.first_trip:
first_trip = coordinator.data.first_trip
device_data["first_trip"] = {
"departure_time_planned": str(first_trip.departure_time_planned)
if first_trip.departure_time_planned
else None,
"departure_time_actual": str(first_trip.departure_time_actual)
if first_trip.departure_time_actual
else None,
"arrival_time_planned": str(first_trip.arrival_time_planned)
if first_trip.arrival_time_planned
else None,
"arrival_time_actual": str(first_trip.arrival_time_actual)
if first_trip.arrival_time_actual
else None,
"departure_platform_planned": first_trip.departure_platform_planned,
"departure_platform_actual": first_trip.departure_platform_actual,
"arrival_platform_planned": first_trip.arrival_platform_planned,
"arrival_platform_actual": first_trip.arrival_platform_actual,
"status": str(first_trip.status) if first_trip.status else None,
"nr_transfers": first_trip.nr_transfers,
"going": first_trip.going,
}
return device_data

View File

@@ -27,6 +27,8 @@ from .const import (
DATA_CAMERAS,
DATA_EVENTS,
DOMAIN,
EVENT_TYPE_CONNECTION,
EVENT_TYPE_DISCONNECTION,
EVENT_TYPE_LIGHT_MODE,
EVENT_TYPE_OFF,
EVENT_TYPE_ON,
@@ -123,7 +125,13 @@ class NetatmoCamera(NetatmoModuleEntity, Camera):
"""Entity created."""
await super().async_added_to_hass()
for event_type in (EVENT_TYPE_LIGHT_MODE, EVENT_TYPE_OFF, EVENT_TYPE_ON):
for event_type in (
EVENT_TYPE_LIGHT_MODE,
EVENT_TYPE_OFF,
EVENT_TYPE_ON,
EVENT_TYPE_CONNECTION,
EVENT_TYPE_DISCONNECTION,
):
self.async_on_remove(
async_dispatcher_connect(
self.hass,
@@ -146,12 +154,19 @@ class NetatmoCamera(NetatmoModuleEntity, Camera):
data["home_id"] == self.home.entity_id
and data["camera_id"] == self.device.entity_id
):
if data[WEBHOOK_PUSH_TYPE] in ("NACamera-off", "NACamera-disconnection"):
if data[WEBHOOK_PUSH_TYPE] in (
"NACamera-off",
"NOCamera-off",
"NACamera-disconnection",
"NOCamera-disconnection",
):
self._attr_is_streaming = False
self._monitoring = False
elif data[WEBHOOK_PUSH_TYPE] in (
"NACamera-on",
"NOCamera-on",
WEBHOOK_NACAMERA_CONNECTION,
"NOCamera-connection",
):
self._attr_is_streaming = True
self._monitoring = True

View File

@@ -127,6 +127,9 @@ EVENT_TYPE_ALARM_STARTED = "alarm_started"
EVENT_TYPE_DOOR_TAG_BIG_MOVE = "tag_big_move"
EVENT_TYPE_DOOR_TAG_OPEN = "tag_open"
EVENT_TYPE_DOOR_TAG_SMALL_MOVE = "tag_small_move"
# Generic events
EVENT_TYPE_CONNECTION = "connection"
EVENT_TYPE_DISCONNECTION = "disconnection"
EVENT_TYPE_OFF = "off"
EVENT_TYPE_ON = "on"

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["pynintendoauth", "pynintendoparental"],
"quality_scale": "bronze",
"requirements": ["pynintendoauth==1.0.0", "pynintendoparental==2.1.0"]
"requirements": ["pynintendoauth==1.0.0", "pynintendoparental==2.1.1"]
}

View File

@@ -9,5 +9,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["openai==2.9.0", "python-open-router==0.3.3"]
"requirements": ["openai==2.11.0", "python-open-router==0.3.3"]
}

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/openai_conversation",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["openai==2.9.0"]
"requirements": ["openai==2.11.0"]
}

View File

@@ -9,5 +9,5 @@
"integration_type": "service",
"iot_class": "local_push",
"quality_scale": "platinum",
"requirements": ["python-overseerr==0.7.1"]
"requirements": ["python-overseerr==0.8.0"]
}

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from plugwise.constants import GwEntityData
from plugwise import GwEntityData
from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
from homeassistant.helpers.device_registry import (
@@ -30,37 +30,43 @@ class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]):
super().__init__(coordinator)
self._dev_id = device_id
configuration_url: str | None = None
if entry := self.coordinator.config_entry:
configuration_url = f"http://{entry.data[CONF_HOST]}"
api = coordinator.api
gateway_id = api.gateway_id
entry = coordinator.config_entry
data = coordinator.data[device_id]
# Link configuration-URL for the gateway device
configuration_url = (
f"http://{entry.data[CONF_HOST]}"
if device_id == gateway_id and entry
else None
)
# Build connections set
connections = set()
if mac := data.get("mac_address"):
if mac := self.device.get("mac_address"):
connections.add((CONNECTION_NETWORK_MAC, mac))
if mac := data.get("zigbee_mac_address"):
connections.add((CONNECTION_ZIGBEE, mac))
if zigbee_mac := self.device.get("zigbee_mac_address"):
connections.add((CONNECTION_ZIGBEE, zigbee_mac))
# Set base device info
self._attr_device_info = DeviceInfo(
configuration_url=configuration_url,
identifiers={(DOMAIN, device_id)},
connections=connections,
manufacturer=data.get("vendor"),
model=data.get("model"),
model_id=data.get("model_id"),
name=coordinator.api.smile.name,
sw_version=data.get("firmware"),
hw_version=data.get("hardware"),
manufacturer=self.device.get("vendor"),
model=self.device.get("model"),
model_id=self.device.get("model_id"),
name=api.smile.name,
sw_version=self.device.get("firmware"),
hw_version=self.device.get("hardware"),
)
if device_id != coordinator.api.gateway_id:
# Add extra info if not the gateway device
if device_id != gateway_id:
self._attr_device_info.update(
{
ATTR_NAME: data.get(ATTR_NAME),
ATTR_VIA_DEVICE: (
DOMAIN,
str(self.coordinator.api.gateway_id),
),
ATTR_NAME: self.device.get(ATTR_NAME),
ATTR_VIA_DEVICE: (DOMAIN, gateway_id),
}
)

View File

@@ -37,7 +37,6 @@ SELECT_TYPES = (
PlugwiseSelectEntityDescription(
key=SELECT_SCHEDULE,
translation_key=SELECT_SCHEDULE,
entity_category=EntityCategory.CONFIG,
options_key="available_schedules",
),
PlugwiseSelectEntityDescription(

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["ical"],
"quality_scale": "silver",
"requirements": ["ical==11.1.0"]
"requirements": ["ical==12.1.1"]
}

View File

@@ -394,7 +394,14 @@ class RoborockWashingMachineUpdateCoordinator(
async def _async_update_data(
self,
) -> dict[RoborockZeoProtocol, StateType]:
try:
return await self.api.query_values(self.request_protocols)
except RoborockException as ex:
_LOGGER.debug("Failed to update washing machine data: %s", ex)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_data_fail",
) from ex
class RoborockWetDryVacUpdateCoordinator(
@@ -425,4 +432,11 @@ class RoborockWetDryVacUpdateCoordinator(
async def _async_update_data(
self,
) -> dict[RoborockDyadDataProtocol, StateType]:
try:
return await self.api.query_values(self.request_protocols)
except RoborockException as ex:
_LOGGER.debug("Failed to update wet dry vac data: %s", ex)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_data_fail",
) from ex

View File

@@ -416,8 +416,8 @@ def warn_dip(
_LOGGER.warning(
(
"Entity %s %shas state class total_increasing, but its state is not"
" strictly increasing. Triggered by state %s (%s) with last_updated set"
" to %s. Please %s"
" strictly increasing. Triggered by state %s (previous state: %s) with"
" last_updated set to %s. Please %s"
),
entity_id,
f"from integration {domain} " if domain else "",

View File

@@ -63,6 +63,7 @@ from .repairs import (
async_manage_open_wifi_ap_issue,
async_manage_outbound_websocket_incorrectly_enabled_issue,
)
from .services import async_setup_services
from .utils import (
async_create_issue_unsupported_firmware,
async_migrate_rpc_virtual_components_unique_ids,
@@ -117,6 +118,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if (conf := config.get(DOMAIN)) is not None:
hass.data[DOMAIN] = {CONF_COAP_PORT: conf[CONF_COAP_PORT]}
async_setup_services(hass)
return True

View File

@@ -343,3 +343,6 @@ MODEL_FRANKEVER_IRRIGATION_CONTROLLER = "Irrigation"
ROLE_GENERIC = "generic"
TRV_CHANNEL = 0
ATTR_KEY = "key"
ATTR_VALUE = "value"

View File

@@ -105,5 +105,13 @@
}
}
}
},
"services": {
"get_kvs_value": {
"service": "mdi:import"
},
"set_kvs_value": {
"service": "mdi:export"
}
}
}

View File

@@ -1,17 +1,13 @@
rules:
# Bronze
action-setup:
status: exempt
comment: The integration does not register services.
action-setup: done
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: The integration does not register services.
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
@@ -24,9 +20,7 @@ rules:
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: The integration does not register services.
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done

View File

@@ -0,0 +1,170 @@
"""Support for services."""
from typing import TYPE_CHECKING, Any, cast
from aioshelly.const import RPC_GENERATIONS
from aioshelly.exceptions import DeviceConnectionError, RpcCallError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.util.json import JsonValueType
from .const import ATTR_KEY, ATTR_VALUE, CONF_SLEEP_PERIOD, DOMAIN
from .coordinator import ShellyConfigEntry
from .utils import get_device_entry_gen
SERVICE_GET_KVS_VALUE = "get_kvs_value"
SERVICE_SET_KVS_VALUE = "set_kvs_value"
SERVICE_GET_KVS_VALUE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_DEVICE_ID): cv.string,
vol.Required(ATTR_KEY): str,
}
)
SERVICE_SET_KVS_VALUE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_DEVICE_ID): cv.string,
vol.Required(ATTR_KEY): str,
vol.Required(ATTR_VALUE): vol.Any(str, int, float, bool, dict, list, None),
}
)
@callback
def async_get_config_entry_for_service_call(
call: ServiceCall,
) -> ShellyConfigEntry:
"""Get the config entry related to a service call (by device ID)."""
device_registry = dr.async_get(call.hass)
device_id = call.data[ATTR_DEVICE_ID]
if (device_entry := device_registry.async_get(device_id)) is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_device_id",
translation_placeholders={"device_id": device_id},
)
for entry_id in device_entry.config_entries:
config_entry = call.hass.config_entries.async_get_entry(entry_id)
if TYPE_CHECKING:
assert config_entry
if config_entry.domain != DOMAIN:
continue
if config_entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="entry_not_loaded",
translation_placeholders={"device": config_entry.title},
)
if get_device_entry_gen(config_entry) not in RPC_GENERATIONS:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="kvs_not_supported",
translation_placeholders={"device": config_entry.title},
)
if config_entry.data.get(CONF_SLEEP_PERIOD, 0) > 0:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="kvs_not_supported",
translation_placeholders={"device": config_entry.title},
)
return config_entry
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="config_entry_not_found",
translation_placeholders={"device_id": device_id},
)
async def _async_execute_action(
call: ServiceCall, method: str, args: tuple
) -> dict[str, Any]:
"""Execute action on the device."""
config_entry = async_get_config_entry_for_service_call(call)
runtime_data = config_entry.runtime_data
if not runtime_data.rpc:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="device_not_initialized",
translation_placeholders={"device": config_entry.title},
)
action_method = getattr(runtime_data.rpc.device, method)
try:
response = await action_method(*args)
except RpcCallError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="rpc_call_error",
translation_placeholders={"device": config_entry.title},
) from err
except DeviceConnectionError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="device_communication_error",
translation_placeholders={"device": config_entry.title},
) from err
else:
return cast(dict[str, Any], response)
async def async_get_kvs_value(call: ServiceCall) -> ServiceResponse:
"""Handle the get_kvs_value service call."""
key = call.data[ATTR_KEY]
response = await _async_execute_action(call, "kvs_get", (key,))
result: dict[str, JsonValueType] = {}
result[ATTR_VALUE] = response[ATTR_VALUE]
return result
async def async_set_kvs_value(call: ServiceCall) -> None:
"""Handle the set_kvs_value service call."""
await _async_execute_action(
call, "kvs_set", (call.data[ATTR_KEY], call.data[ATTR_VALUE])
)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the services for Shelly integration."""
for service, method, schema, response in (
(
SERVICE_GET_KVS_VALUE,
async_get_kvs_value,
SERVICE_GET_KVS_VALUE_SCHEMA,
SupportsResponse.ONLY,
),
(
SERVICE_SET_KVS_VALUE,
async_set_kvs_value,
SERVICE_SET_KVS_VALUE_SCHEMA,
SupportsResponse.NONE,
),
):
hass.services.async_register(
DOMAIN,
service,
method,
schema=schema,
supports_response=response,
)

View File

@@ -0,0 +1,27 @@
get_kvs_value:
fields:
device_id:
required: true
selector:
device:
integration: shelly
key:
required: true
selector:
text:
set_kvs_value:
fields:
device_id:
required: true
selector:
device:
integration: shelly
key:
required: true
selector:
text:
value:
required: true
selector:
object:

View File

@@ -603,6 +603,9 @@
"auth_error": {
"message": "Authentication failed for {device}, please update your credentials"
},
"config_entry_not_found": {
"message": "Config entry for device ID {device_id} not found"
},
"device_communication_action_error": {
"message": "Device communication error occurred while calling action for {entity} of {device}"
},
@@ -612,12 +615,24 @@
"device_not_found": {
"message": "{device} not found while configuring device automation triggers"
},
"device_not_initialized": {
"message": "{device} not initialized"
},
"entry_not_loaded": {
"message": "Config entry not loaded for {device}"
},
"firmware_unsupported": {
"message": "{device} is running an unsupported firmware, please update the firmware"
},
"invalid_device_id": {
"message": "Invalid device ID specified: {device_id}"
},
"invalid_trigger": {
"message": "Invalid device automation trigger (type, subtype): {trigger}"
},
"kvs_not_supported": {
"message": "{device} does not support KVS"
},
"ota_update_connection_error": {
"message": "Device communication error occurred while triggering OTA update for {device}"
},
@@ -627,6 +642,9 @@
"rpc_call_action_error": {
"message": "RPC call error occurred while calling action for {entity} of {device}"
},
"rpc_call_error": {
"message": "RPC call error occurred for {device}"
},
"update_error": {
"message": "An error occurred while retrieving data from {device}"
},
@@ -748,5 +766,39 @@
"manual": "Enter address manually"
}
}
},
"services": {
"get_kvs_value": {
"description": "Get a value from the device's Key-Value Storage.",
"fields": {
"device_id": {
"description": "The ID of the Shelly device to get the KVS value from.",
"name": "Device"
},
"key": {
"description": "The name of the key for which the KVS value will be retrieved.",
"name": "Key"
}
},
"name": "Get KVS value"
},
"set_kvs_value": {
"description": "Set a value in the device's Key-Value Storage.",
"fields": {
"device_id": {
"description": "The ID of the Shelly device to set the KVS value.",
"name": "Device"
},
"key": {
"description": "The name of the key under which the KVS value will be stored.",
"name": "Key"
},
"value": {
"description": "Value to set.",
"name": "Value"
}
},
"name": "Set KVS value"
}
}
}

View File

@@ -31,5 +31,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"quality_scale": "bronze",
"requirements": ["pysmartthings==3.5.0"]
"requirements": ["pysmartthings==3.5.1"]
}

View File

@@ -155,7 +155,10 @@ class SqueezeboxConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unknown exception while validating connection")
return "unknown"
if "uuid" in status:
if "uuid" not in status:
_LOGGER.exception("Discovered server did not provide a uuid")
return "missing_uuid"
await self.async_set_unique_id(status["uuid"])
self._abort_if_unique_id_configured()

View File

@@ -7,6 +7,7 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"missing_uuid": "Your LMS did not provide a unique identifier and is not compatible with this integration. Please check and update your LMS version.",
"no_server_found": "Could not automatically discover server.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},

View File

@@ -29,5 +29,13 @@
"turn_on": {
"service": "mdi:toggle-switch-variant"
}
},
"triggers": {
"turned_off": {
"trigger": "mdi:toggle-switch-variant-off"
},
"turned_on": {
"trigger": "mdi:toggle-switch-variant"
}
}
}

View File

@@ -1,4 +1,8 @@
{
"common": {
"trigger_behavior_description": "The behavior of the targeted switches to trigger on.",
"trigger_behavior_name": "Behavior"
},
"device_automation": {
"action_type": {
"toggle": "[%key:common::device_automation::action_type::toggle%]",
@@ -41,6 +45,15 @@
}
}
},
"selector": {
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"toggle": {
"description": "Toggles a switch on/off.",
@@ -55,5 +68,27 @@
"name": "[%key:common::action::turn_on%]"
}
},
"title": "Switch"
"title": "Switch",
"triggers": {
"turned_off": {
"description": "Triggers after one or more switches turn off.",
"fields": {
"behavior": {
"description": "[%key:component::switch::common::trigger_behavior_description%]",
"name": "[%key:component::switch::common::trigger_behavior_name%]"
}
},
"name": "Switch turned off"
},
"turned_on": {
"description": "Triggers after one or more switches turn on.",
"fields": {
"behavior": {
"description": "[%key:component::switch::common::trigger_behavior_description%]",
"name": "[%key:component::switch::common::trigger_behavior_name%]"
}
},
"name": "Switch turned on"
}
}
}

View File

@@ -0,0 +1,17 @@
"""Provides triggers for switch platform."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.trigger import Trigger, make_entity_state_trigger
from .const import DOMAIN
TRIGGERS: dict[str, type[Trigger]] = {
"turned_on": make_entity_state_trigger(DOMAIN, STATE_ON),
"turned_off": make_entity_state_trigger(DOMAIN, STATE_OFF),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for switch platform."""
return TRIGGERS

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