Compare commits

..

61 Commits

Author SHA1 Message Date
Franck Nijhof 9dbf84228e 2024.2.0 (#109883) 2024-02-07 18:31:28 +01:00
Joost Lekkerkerker 9e47d03086 Fix kitchen sink tests (#109243) 2024-02-07 17:40:10 +01:00
Franck Nijhof f63aaf8b5a Bump version to 2024.2.0 2024-02-07 16:28:11 +01:00
Malte Franken 8375fc235d Bump aio-geojson-geonetnz-quakes to 0.16 (#109873) 2024-02-07 16:27:47 +01:00
Åke Strandberg 3030870de0 Remove soft hyphens from myuplink sensor names (#109845)
Remove soft hyphens from sensor names
2024-02-07 16:27:44 +01:00
Matrix f61c70b686 Fix YoLink SpeakerHub support (#107925)
* improve

* Fix when hub offline/online message pushing

* fix as suggestion

* check config entry load state

* Add exception translation
2024-02-07 16:27:40 +01:00
Franck Nijhof e720b398d2 Bump version to 2024.2.0b11 2024-02-07 12:44:50 +01:00
Bram Kragten bd21490a57 Update frontend to 20240207.0 (#109871) 2024-02-07 12:34:36 +01:00
Malte Franken 75b308c1aa Bump aio-georss-gdacs to 0.9 (#109859) 2024-02-07 12:34:33 +01:00
Joakim Plate 881707e1fe Update nibe to 2.8.0 with LOG.SET fixes (#109825)
Update nibe to 2.8.0
2024-02-07 12:34:30 +01:00
Jiayi Chen 2ca3bbaea5 Update Growatt server URLs (#109122) 2024-02-07 12:34:24 +01:00
Franck Nijhof ea4bdbb3a0 Bump version to 2024.2.0b10 2024-02-07 08:48:17 +01:00
puddly 40cfc31dcb Bump ZHA dependency zigpy to 0.62.3 (#109848) 2024-02-07 08:48:07 +01:00
starkillerOG 031aadff00 Bump motionblinds to 0.6.20 (#109837) 2024-02-07 08:48:04 +01:00
Vilppu Vuorinen 27691b7d48 Disable energy report based operations with API lib upgrade (#109832)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2024-02-07 08:48:01 +01:00
Joost Lekkerkerker fe94107af7 Make integration fields in Analytics Insights optional (#109789) 2024-02-07 08:47:58 +01:00
Teemu R d784a76d32 Add tapo virtual integration (#109765) 2024-02-07 08:47:55 +01:00
Joost Lekkerkerker ebb1912617 Show domain in oauth2 error log (#109708)
* Show token url in oauth2 error log

* Fix tests

* Use domain
2024-02-07 08:47:50 +01:00
Franck Nijhof 8c605c29c3 Bump version to 2024.2.0b9 2024-02-06 22:49:53 +01:00
Joakim Sørensen 74a75e709f Bump awesomeversion from 23.11.0 to 24.2.0 (#109830) 2024-02-06 22:49:41 +01:00
J. Nick Koston 2103875ff7 Bump aioesphomeapi to 21.0.2 (#109824) 2024-02-06 22:49:38 +01:00
Erik Montnemery 5c83b774bb Bump python-otbr-api to 2.6.0 (#109823) 2024-02-06 22:49:34 +01:00
Joost Lekkerkerker 2c870f9da9 Bump aioecowitt to 2024.2.0 (#109817) 2024-02-06 22:49:31 +01:00
Maciej Bieniek 40adb3809f Ignore trackable without details in Tractive integration (#109814)
Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-02-06 22:49:28 +01:00
wittypluck 8aa1242221 Mark Unifi bandwidth sensors as unavailable when client disconnects (#109812)
* Set sensor as unavailable instead of resetting value to 0 on disconnect

* Update unit test on unavailable bandwidth sensor
2024-02-06 22:49:25 +01:00
J. Nick Koston 8569ddc5f9 Fix entity services targeting entities outside the platform when using areas/devices (#109810) 2024-02-06 22:49:22 +01:00
Franck Nijhof 7032415528 Don't block Supervisor entry setup with refreshing updates (#109809) 2024-02-06 22:49:19 +01:00
puddly d099fb2a26 Pin chacha20poly1305-reuseable>=0.12.1 (#109807)
* Pin `chacha20poly1305-reuseable`
Prevents a runtime `assert isinstance(cipher, AESGCM)` error

* Update `gen_requirements_all.py` as well
2024-02-06 22:49:16 +01:00
Jan-Philipp Benecke 35fad52913 Bump aioelectricitymaps to 0.3.1 (#109797) 2024-02-06 22:49:13 +01:00
Vilppu Vuorinen c170132827 Update MELCloud codeowners (#109793)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2024-02-06 22:49:10 +01:00
Matthias Alphart 439f82a4ec Update xknx to 2.12.0 and xknxproject to 3.5.0 (#109787) 2024-02-06 22:49:07 +01:00
Steven B 2481d14632 Bump ring_doorbell to 0.8.7 (#109783) 2024-02-06 22:49:04 +01:00
Steven B 3cf826dc93 Bump ring_doorbell to 0.8.6 (#109199) 2024-02-06 22:48:59 +01:00
Jan-Philipp Benecke e25ddf9650 Change state class of Tesla wall connector session energy entity (#109778) 2024-02-06 22:46:34 +01:00
puddly 8d79ac67f5 Bump ZHA dependencies (#109770)
* Bump ZHA dependencies

* Bump universal-silabs-flasher to 0.0.18

* Flip `Server_to_Client` enum in ZHA unit test

* Bump zigpy to 0.62.2
2024-02-06 22:46:31 +01:00
David F. Mulcahey 5025c15165 Buffer JsonDecodeError in Flo (#109767) 2024-02-06 22:46:28 +01:00
Joost Lekkerkerker ffd5e04a29 Fix Radarr health check singularity (#109762)
* Fix Radarr health check singularity

* Fix comment
2024-02-06 22:46:25 +01:00
G Johansson 9fcdfd1b16 Bump holidays to 0.42 (#109760) 2024-02-06 22:46:21 +01:00
Vilppu Vuorinen c1e5b2e6cc Fix compatibility issues with older pymelcloud version (#109757) 2024-02-06 22:42:58 +01:00
suaveolent 31c0d21204 Improve lupusec code quality (#109727)
* renamed async_add_devices

* fixed typo

* patch class instead of __init__

* ensure non blocking get_alarm

* exception handling

* added test case for json decode error

* avoid blockign calls

---------

Co-authored-by: suaveolent <suaveolent@users.noreply.github.com>
2024-02-06 22:42:54 +01:00
spycle 3ba63fc78f Fix keymitt_ble config-flow (#109644)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-02-06 22:42:51 +01:00
spycle 0395315267 Bump pyMicrobot to 0.0.10 (#109628) 2024-02-06 22:42:48 +01:00
TheJulianJES 6b354457c2 Fix ZHA creating unnecessary "summ received" entity after upgrade (#109268)
* Do not create `current_summ_received` entity until initialized once

* Update zha_devices_list.py to not expect summation received entities

The attribute isn't initialized for these devices in the test (which our check now expects it to be), hence we need to remove them from this list.

* Update sensor tests to have initial state for current_summ_received entity

The attribute needs to be initialized for it to be created which we do by plugging the attribute read.
The test expects the initial state to be "unknown", but hence we plugged the attribute (to create the entity), the state is whatever we plug the attribute read as.

* Update sensor tests to expect not updating current_summ_received entity if it doesn't exist
2024-02-06 22:42:43 +01:00
Franck Nijhof df88335370 Bump version to 2024.2.0b8 2024-02-05 20:27:40 +01:00
Joost Lekkerkerker 4c6c5ee63d Handle startup error in Analytics insights (#109755) 2024-02-05 20:27:29 +01:00
Vilppu Vuorinen 65476914ed Reduce MELCloud poll frequency to avoid throttling (#109750) 2024-02-05 20:27:26 +01:00
Bouwe Westerdijk d30a2e3611 Fix incorrectly assigning supported features for plugwise climates (#109749) 2024-02-05 20:27:23 +01:00
G Johansson eb510e3630 Add missing new climate feature flags to Mill (#109748) 2024-02-05 20:27:20 +01:00
Michael 532df5b5f1 Use tracked entity friendly name for proximity sensors (#109744)
user tracked entity friendly name
2024-02-05 20:27:17 +01:00
Jan Bouwhuis 1534f99c80 Fix generic camera error when template renders to an invalid URL (#109737) 2024-02-05 20:27:14 +01:00
Cyrill Raccaud a19aa9595a Bump python-bring-api to 3.0.0 (#109720) 2024-02-05 20:27:11 +01:00
Joost Lekkerkerker e3191d098f Add strings to Ruuvitag BLE (#109717) 2024-02-05 20:27:08 +01:00
Bram Kragten cc36071612 Update frontend to 20240205.0 (#109716) 2024-02-05 20:27:04 +01:00
Joakim Sørensen 2d90ee8237 Fix log string in Traccar Server Coordinator (#109709) 2024-02-05 20:27:01 +01:00
Simone Chemelli 16266703df Queue climate calls for Comelit SimpleHome (#109707) 2024-02-05 20:26:58 +01:00
Joost Lekkerkerker dd2cc52119 Set Analytics Insights as diagnostic (#109702)
* Set Analytics Insights as diagnostic

* Set Analytics Insights as diagnostic
2024-02-05 20:26:54 +01:00
Joost Lekkerkerker c48c8c25fa Remove obsolete check from Proximity (#109701) 2024-02-05 20:26:51 +01:00
Joost Lekkerkerker 83a5659d57 Set shorthand attribute in Epion (#109695) 2024-02-05 20:26:48 +01:00
Joost Lekkerkerker 3183cd346d Add data descriptions to analytics insights (#109694) 2024-02-05 20:26:45 +01:00
Marcel van der Veldt 5930c841d7 Bump python matter server to 5.4.1 (#109692) 2024-02-05 20:26:42 +01:00
Myles Eftos f05ba22b5c Show site state in Amberelectric config flow (#104702) 2024-02-05 20:26:38 +01:00
100 changed files with 1052 additions and 414 deletions
+1
View File
@@ -485,6 +485,7 @@ omit =
homeassistant/components/gpsd/sensor.py
homeassistant/components/greenwave/light.py
homeassistant/components/growatt_server/__init__.py
homeassistant/components/growatt_server/const.py
homeassistant/components/growatt_server/sensor.py
homeassistant/components/growatt_server/sensor_types/*
homeassistant/components/gstreamer/media_player.py
-2
View File
@@ -786,8 +786,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/media_source/ @hunterjm
/tests/components/media_source/ @hunterjm
/homeassistant/components/mediaroom/ @dgomes
/homeassistant/components/melcloud/ @vilppuvuorinen
/tests/components/melcloud/ @vilppuvuorinen
/homeassistant/components/melissa/ @kennedyshead
/tests/components/melissa/ @kennedyshead
/homeassistant/components/melnor/ @vanstinator
+1 -1
View File
@@ -1,6 +1,6 @@
{
"domain": "tplink",
"name": "TP-Link",
"integrations": ["tplink", "tplink_omada", "tplink_lte"],
"integrations": ["tplink", "tplink_omada", "tplink_lte", "tplink_tapo"],
"iot_standards": ["matter"]
}
@@ -3,18 +3,46 @@ from __future__ import annotations
import amberelectric
from amberelectric.api import amber_api
from amberelectric.model.site import Site
from amberelectric.model.site import Site, SiteStatus
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_TOKEN
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
from .const import CONF_SITE_ID, CONF_SITE_NAME, CONF_SITE_NMI, DOMAIN
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN
API_URL = "https://app.amber.com.au/developers"
def generate_site_selector_name(site: Site) -> str:
"""Generate the name to show in the site drop down in the configuration flow."""
if site.status == SiteStatus.CLOSED:
return site.nmi + " (Closed: " + site.closed_on.isoformat() + ")" # type: ignore[no-any-return]
if site.status == SiteStatus.PENDING:
return site.nmi + " (Pending)" # type: ignore[no-any-return]
return site.nmi # type: ignore[no-any-return]
def filter_sites(sites: list[Site]) -> list[Site]:
"""Deduplicates the list of sites."""
filtered: list[Site] = []
filtered_nmi: set[str] = set()
for site in sorted(sites, key=lambda site: site.status.value):
if site.status == SiteStatus.ACTIVE or site.nmi not in filtered_nmi:
filtered.append(site)
filtered_nmi.add(site.nmi)
return filtered
class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
@@ -31,7 +59,7 @@ class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
api: amber_api.AmberApi = amber_api.AmberApi.create(configuration)
try:
sites: list[Site] = api.get_sites()
sites: list[Site] = filter_sites(api.get_sites())
if len(sites) == 0:
self._errors[CONF_API_TOKEN] = "no_site"
return None
@@ -86,38 +114,31 @@ class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
assert self._sites is not None
assert self._api_token is not None
api_token = self._api_token
if user_input is not None:
site_nmi = user_input[CONF_SITE_NMI]
sites = [site for site in self._sites if site.nmi == site_nmi]
site = sites[0]
site_id = site.id
site_id = user_input[CONF_SITE_ID]
name = user_input.get(CONF_SITE_NAME, site_id)
return self.async_create_entry(
title=name,
data={
CONF_SITE_ID: site_id,
CONF_API_TOKEN: api_token,
CONF_SITE_NMI: site.nmi,
},
data={CONF_SITE_ID: site_id, CONF_API_TOKEN: self._api_token},
)
user_input = {
CONF_API_TOKEN: api_token,
CONF_SITE_NMI: "",
CONF_SITE_NAME: "",
}
return self.async_show_form(
step_id="site",
data_schema=vol.Schema(
{
vol.Required(
CONF_SITE_NMI, default=user_input[CONF_SITE_NMI]
): vol.In([site.nmi for site in self._sites]),
vol.Optional(
CONF_SITE_NAME, default=user_input[CONF_SITE_NAME]
): str,
vol.Required(CONF_SITE_ID): SelectSelector(
SelectSelectorConfig(
options=[
SelectOptionDict(
value=site.id,
label=generate_site_selector_name(site),
)
for site in self._sites
],
mode=SelectSelectorMode.DROPDOWN,
)
),
vol.Optional(CONF_SITE_NAME): str,
}
),
errors=self._errors,
@@ -6,7 +6,6 @@ from homeassistant.const import Platform
DOMAIN = "amberelectric"
CONF_SITE_NAME = "site_name"
CONF_SITE_ID = "site_id"
CONF_SITE_NMI = "site_nmi"
ATTRIBUTION = "Data provided by Amber Electric"
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
"iot_class": "cloud_polling",
"loggers": ["amberelectric"],
"requirements": ["amberelectric==1.0.4"]
"requirements": ["amberelectric==1.1.0"]
}
@@ -3,11 +3,15 @@ from __future__ import annotations
from dataclasses import dataclass
from python_homeassistant_analytics import HomeassistantAnalyticsClient
from python_homeassistant_analytics import (
HomeassistantAnalyticsClient,
HomeassistantAnalyticsConnectionError,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_TRACKED_INTEGRATIONS, DOMAIN
@@ -28,7 +32,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Homeassistant Analytics from a config entry."""
client = HomeassistantAnalyticsClient(session=async_get_clientsession(hass))
integrations = await client.get_integrations()
try:
integrations = await client.get_integrations()
except HomeassistantAnalyticsConnectionError as ex:
raise ConfigEntryNotReady("Could not fetch integration list") from ex
names = {}
for integration in entry.options[CONF_TRACKED_INTEGRATIONS]:
@@ -53,10 +53,25 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
) -> FlowResult:
"""Handle the initial step."""
self._async_abort_entries_match()
if user_input:
return self.async_create_entry(
title="Home Assistant Analytics Insights", data={}, options=user_input
)
errors: dict[str, str] = {}
if user_input is not None:
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
CONF_TRACKED_CUSTOM_INTEGRATIONS
):
errors["base"] = "no_integrations_selected"
else:
return self.async_create_entry(
title="Home Assistant Analytics Insights",
data={},
options={
CONF_TRACKED_INTEGRATIONS: user_input.get(
CONF_TRACKED_INTEGRATIONS, []
),
CONF_TRACKED_CUSTOM_INTEGRATIONS: user_input.get(
CONF_TRACKED_CUSTOM_INTEGRATIONS, []
),
},
)
client = HomeassistantAnalyticsClient(
session=async_get_clientsession(self.hass)
@@ -78,16 +93,17 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
]
return self.async_show_form(
step_id="user",
errors=errors,
data_schema=vol.Schema(
{
vol.Required(CONF_TRACKED_INTEGRATIONS): SelectSelector(
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
SelectSelectorConfig(
options=options,
multiple=True,
sort=True,
)
),
vol.Required(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
vol.Optional(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
SelectSelectorConfig(
options=list(custom_integrations),
multiple=True,
@@ -106,8 +122,24 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if user_input:
return self.async_create_entry(title="", data=user_input)
errors: dict[str, str] = {}
if user_input is not None:
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
CONF_TRACKED_CUSTOM_INTEGRATIONS
):
errors["base"] = "no_integrations_selected"
else:
return self.async_create_entry(
title="",
data={
CONF_TRACKED_INTEGRATIONS: user_input.get(
CONF_TRACKED_INTEGRATIONS, []
),
CONF_TRACKED_CUSTOM_INTEGRATIONS: user_input.get(
CONF_TRACKED_CUSTOM_INTEGRATIONS, []
),
},
)
client = HomeassistantAnalyticsClient(
session=async_get_clientsession(self.hass)
@@ -129,17 +161,18 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
]
return self.async_show_form(
step_id="init",
errors=errors,
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Required(CONF_TRACKED_INTEGRATIONS): SelectSelector(
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
SelectSelectorConfig(
options=options,
multiple=True,
sort=True,
)
),
vol.Required(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
vol.Optional(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
SelectSelectorConfig(
options=list(custom_integrations),
multiple=True,
@@ -10,6 +10,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -93,6 +94,7 @@ class HomeassistantAnalyticsSensor(
"""Home Assistant Analytics Sensor."""
_attr_has_entity_name = True
_attr_entity_category = EntityCategory.DIAGNOSTIC
entity_description: AnalyticsSensorEntityDescription
@@ -5,24 +5,39 @@
"data": {
"tracked_integrations": "Integrations",
"tracked_custom_integrations": "Custom integrations"
},
"data_description": {
"tracked_integrations": "Select the integrations you want to track",
"tracked_custom_integrations": "Select the custom integrations you want to track"
}
}
},
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"error": {
"no_integration_selected": "You must select at least one integration to track"
}
},
"options": {
"step": {
"init": {
"data": {
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]"
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]",
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_custom_integrations%]"
},
"data_description": {
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_integrations%]",
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_custom_integrations%]"
}
}
},
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"error": {
"no_integration_selected": "[%key:component::analytics_insights::config::error::no_integration_selected%]"
}
},
"entity": {
+5 -6
View File
@@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .coordinator import BringDataUpdateCoordinator
@@ -29,14 +30,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
email = entry.data[CONF_EMAIL]
password = entry.data[CONF_PASSWORD]
bring = Bring(email, password)
def login_and_load_lists() -> None:
bring.login()
bring.loadLists()
session = async_get_clientsession(hass)
bring = Bring(email, password, sessionAsync=session)
try:
await hass.async_add_executor_job(login_and_load_lists)
await bring.loginAsync()
await bring.loadListsAsync()
except BringRequestException as e:
raise ConfigEntryNotReady(
f"Timeout while connecting for email '{email}'"
@@ -11,6 +11,7 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
TextSelector,
TextSelectorConfig,
@@ -48,14 +49,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
bring = Bring(user_input[CONF_EMAIL], user_input[CONF_PASSWORD])
def login_and_load_lists() -> None:
bring.login()
bring.loadLists()
session = async_get_clientsession(self.hass)
bring = Bring(
user_input[CONF_EMAIL], user_input[CONF_PASSWORD], sessionAsync=session
)
try:
await self.hass.async_add_executor_job(login_and_load_lists)
await bring.loginAsync()
await bring.loadListsAsync()
except BringRequestException:
errors["base"] = "cannot_connect"
except BringAuthException:
@@ -40,9 +40,7 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
async def _async_update_data(self) -> dict[str, BringData]:
try:
lists_response = await self.hass.async_add_executor_job(
self.bring.loadLists
)
lists_response = await self.bring.loadListsAsync()
except BringRequestException as e:
raise UpdateFailed("Unable to connect and retrieve data from bring") from e
except BringParseException as e:
@@ -51,9 +49,7 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
list_dict = {}
for lst in lists_response["lists"]:
try:
items = await self.hass.async_add_executor_job(
self.bring.getItems, lst["listUuid"]
)
items = await self.bring.getItemsAsync(lst["listUuid"])
except BringRequestException as e:
raise UpdateFailed(
"Unable to connect and retrieve data from bring"
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bring",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["python-bring-api==2.0.0"]
"requirements": ["python-bring-api==3.0.0"]
}
+8 -15
View File
@@ -91,11 +91,8 @@ class BringTodoListEntity(
async def async_create_todo_item(self, item: TodoItem) -> None:
"""Add an item to the To-do list."""
try:
await self.hass.async_add_executor_job(
self.coordinator.bring.saveItem,
self.bring_list["listUuid"],
item.summary,
item.description or "",
await self.coordinator.bring.saveItemAsync(
self.bring_list["listUuid"], item.summary, item.description or ""
)
except BringRequestException as e:
raise HomeAssistantError("Unable to save todo item for bring") from e
@@ -126,16 +123,14 @@ class BringTodoListEntity(
assert item.uid
if item.status == TodoItemStatus.COMPLETED:
await self.hass.async_add_executor_job(
self.coordinator.bring.removeItem,
await self.coordinator.bring.removeItemAsync(
bring_list["listUuid"],
item.uid,
)
elif item.summary == item.uid:
try:
await self.hass.async_add_executor_job(
self.coordinator.bring.updateItem,
await self.coordinator.bring.updateItemAsync(
bring_list["listUuid"],
item.uid,
item.description or "",
@@ -144,13 +139,11 @@ class BringTodoListEntity(
raise HomeAssistantError("Unable to update todo item for bring") from e
else:
try:
await self.hass.async_add_executor_job(
self.coordinator.bring.removeItem,
await self.coordinator.bring.removeItemAsync(
bring_list["listUuid"],
item.uid,
)
await self.hass.async_add_executor_job(
self.coordinator.bring.saveItem,
await self.coordinator.bring.saveItemAsync(
bring_list["listUuid"],
item.summary,
item.description or "",
@@ -164,8 +157,8 @@ class BringTodoListEntity(
"""Delete an item from the To-do list."""
for uid in uids:
try:
await self.hass.async_add_executor_job(
self.coordinator.bring.removeItem, self.bring_list["listUuid"], uid
await self.coordinator.bring.removeItemAsync(
self.bring_list["listUuid"], uid
)
except BringRequestException as e:
raise HomeAssistantError("Unable to delete todo item for bring") from e
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["aioelectricitymaps"],
"requirements": ["aioelectricitymaps==0.3.0"]
"requirements": ["aioelectricitymaps==0.3.1"]
}
+1 -4
View File
@@ -1,12 +1,11 @@
"""Support for climates."""
from __future__ import annotations
import asyncio
from enum import StrEnum
from typing import Any
from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.const import CLIMATE, SLEEP_BETWEEN_CALLS
from aiocomelit.const import CLIMATE
from homeassistant.components.climate import (
ClimateEntity,
@@ -191,7 +190,6 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity
await self.coordinator.api.set_clima_status(
self._device.index, ClimaAction.MANUAL
)
await asyncio.sleep(SLEEP_BETWEEN_CALLS)
await self.coordinator.api.set_clima_status(
self._device.index, ClimaAction.SET, target_temp
)
@@ -203,7 +201,6 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity
await self.coordinator.api.set_clima_status(
self._device.index, ClimaAction.ON
)
await asyncio.sleep(SLEEP_BETWEEN_CALLS)
await self.coordinator.api.set_clima_status(
self._device.index, MODE_TO_ACTION[hvac_mode]
)
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/comelit",
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"requirements": ["aiocomelit==0.8.2"]
"requirements": ["aiocomelit==0.8.3"]
}
+2 -2
View File
@@ -38,8 +38,8 @@ class EcowittEntity(Entity):
"""Update the state on callback."""
self.async_write_ha_state()
self.ecowitt.update_cb.append(_update_state) # type: ignore[arg-type] # upstream bug
self.async_on_remove(lambda: self.ecowitt.update_cb.remove(_update_state)) # type: ignore[arg-type] # upstream bug
self.ecowitt.update_cb.append(_update_state)
self.async_on_remove(lambda: self.ecowitt.update_cb.remove(_update_state))
@property
def available(self) -> bool:
@@ -6,5 +6,5 @@
"dependencies": ["webhook"],
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
"iot_class": "local_push",
"requirements": ["aioecowitt==2023.5.0"]
"requirements": ["aioecowitt==2024.2.0"]
}
+2 -2
View File
@@ -88,9 +88,9 @@ class EpionSensor(CoordinatorEntity[EpionCoordinator], SensorEntity):
super().__init__(coordinator)
self._epion_device_id = epion_device_id
self.entity_description = description
self.unique_id = f"{epion_device_id}_{description.key}"
self._attr_unique_id = f"{epion_device_id}_{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._epion_device_id)},
identifiers={(DOMAIN, epion_device_id)},
manufacturer="Epion",
name=self.device.get("deviceName"),
sw_version=self.device.get("fwVersion"),
@@ -15,7 +15,7 @@
"iot_class": "local_push",
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
"requirements": [
"aioesphomeapi==21.0.1",
"aioesphomeapi==21.0.2",
"esphome-dashboard-api==1.2.3",
"bleak-esphome==0.4.1"
],
+2 -1
View File
@@ -7,6 +7,7 @@ from typing import Any
from aioflo.api import API
from aioflo.errors import RequestError
from orjson import JSONDecodeError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -46,7 +47,7 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=
await self._update_device()
await self._update_consumption_data()
self._failure_count = 0
except (RequestError, TimeoutError) as error:
except (RequestError, TimeoutError, JSONDecodeError) as error:
self._failure_count += 1
if self._failure_count > 3:
raise UpdateFailed(error) from error
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20240202.0"]
"requirements": ["home-assistant-frontend==20240207.0"]
}
+1 -1
View File
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aio_georss_gdacs", "aio_georss_client"],
"quality_scale": "platinum",
"requirements": ["aio-georss-gdacs==0.8"]
"requirements": ["aio-georss-gdacs==0.9"]
}
@@ -8,6 +8,7 @@ import logging
from typing import Any
import httpx
import voluptuous as vol
import yarl
from homeassistant.components.camera import Camera, CameraEntityFeature
@@ -140,6 +141,12 @@ class GenericCamera(Camera):
_LOGGER.error("Error parsing template %s: %s", self._still_image_url, err)
return self._last_image
try:
vol.Schema(vol.Url())(url)
except vol.Invalid as err:
_LOGGER.warning("Invalid URL '%s': %s, returning last image", url, err)
return self._last_image
if url == self._last_url and self._limit_refetch:
return self._last_image
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aio_geojson_geonetnz_quakes"],
"quality_scale": "platinum",
"requirements": ["aio-geojson-geonetnz-quakes==0.15"]
"requirements": ["aio-geojson-geonetnz-quakes==0.16"]
}
@@ -8,13 +8,16 @@ DEFAULT_PLANT_ID = "0"
DEFAULT_NAME = "Growatt"
SERVER_URLS = [
"https://server-api.growatt.com/",
"https://server-us.growatt.com/",
"http://server.smten.com/",
"https://openapi.growatt.com/", # Other regional server
"https://openapi-cn.growatt.com/", # Chinese server
"https://openapi-us.growatt.com/", # North American server
"http://server.smten.com/", # smten server
]
DEPRECATED_URLS = [
"https://server.growatt.com/",
"https://server-api.growatt.com/",
"https://server-us.growatt.com/",
]
DEFAULT_URL = SERVER_URLS[0]
+7 -1
View File
@@ -1001,12 +1001,18 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=has
raise_on_entry_error: bool = False,
) -> None:
"""Refresh data."""
if not scheduled:
if not scheduled and not raise_on_auth_failed:
# Force refreshing updates for non-scheduled updates
# If `raise_on_auth_failed` is set, it means this is
# the first refresh and we do not want to delay
# startup or cause a timeout so we only refresh the
# updates if this is not a scheduled refresh and
# we are not doing the first refresh.
try:
await self.hassio.refresh_updates()
except HassioAPIError as err:
_LOGGER.warning("Error on Supervisor API: %s", err)
await super()._async_refresh(
log_failures, raise_on_auth_failed, scheduled, raise_on_entry_error
)
+1 -1
View File
@@ -459,7 +459,7 @@ class HassIO:
This method returns a coroutine.
"""
return self.send_command("/refresh_updates", timeout=None)
return self.send_command("/refresh_updates", timeout=300)
@api_data
def retrieve_discovery_messages(self) -> Coroutine:
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.41", "babel==2.13.1"]
"requirements": ["holidays==0.42", "babel==2.13.1"]
}
@@ -138,6 +138,8 @@ class MicroBotConfigFlow(ConfigFlow, domain=DOMAIN):
await self._client.connect(init=True)
return self.async_show_form(step_id="link")
if not await self._client.is_connected():
await self._client.connect(init=False)
if not await self._client.is_connected():
errors["base"] = "linking"
else:
@@ -13,7 +13,8 @@
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/keymitt_ble",
"integration_type": "hub",
"iot_class": "assumed_state",
"loggers": ["keymitt_ble"],
"requirements": ["PyMicroBot==0.0.9"]
"requirements": ["PyMicroBot==0.0.10"]
}
+2 -2
View File
@@ -11,8 +11,8 @@
"loggers": ["xknx", "xknxproject"],
"quality_scale": "platinum",
"requirements": [
"xknx==2.11.2",
"xknxproject==3.4.0",
"xknx==2.12.0",
"xknxproject==3.5.0",
"knx-frontend==2024.1.20.105944"
]
}
+3 -7
View File
@@ -1,4 +1,5 @@
"""Support for Lupusec Home Security system."""
from json import JSONDecodeError
import logging
import lupupy
@@ -111,16 +112,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
lupusec_system = await hass.async_add_executor_job(
lupupy.Lupusec, username, password, host
)
except LupusecException:
_LOGGER.error("Failed to connect to Lupusec device at %s", host)
return False
except Exception as ex: # pylint: disable=broad-except
_LOGGER.error(
"Unknown error while trying to connect to Lupusec device at %s: %s",
host,
ex,
)
except JSONDecodeError:
_LOGGER.error("Failed to connect to Lupusec device at %s", host)
return False
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = lupusec_system
@@ -29,14 +29,14 @@ SCAN_INTERVAL = timedelta(seconds=2)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_devices: AddEntitiesCallback,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up an alarm control panel for a Lupusec device."""
data = hass.data[DOMAIN][config_entry.entry_id]
alarm_devices = [LupusecAlarm(data, data.get_alarm(), config_entry.entry_id)]
alarm = await hass.async_add_executor_job(data.get_alarm)
async_add_devices(alarm_devices)
async_add_entities([LupusecAlarm(data, alarm, config_entry.entry_id)])
class LupusecAlarm(LupusecDevice, AlarmControlPanelEntity):
@@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import timedelta
from functools import partial
import logging
import lupupy.constants as CONST
@@ -25,7 +26,7 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_devices: AddEntitiesCallback,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a binary sensors for a Lupusec device."""
@@ -34,10 +35,12 @@ async def async_setup_entry(
device_types = CONST.TYPE_OPENING + CONST.TYPE_SENSOR
sensors = []
for device in data.get_devices(generic_type=device_types):
partial_func = partial(data.get_devices, generic_type=device_types)
devices = await hass.async_add_executor_job(partial_func)
for device in devices:
sensors.append(LupusecBinarySensor(device, config_entry.entry_id))
async_add_devices(sensors)
async_add_entities(sensors)
class LupusecBinarySensor(LupusecBaseSensor, BinarySensorEntity):
@@ -1,5 +1,6 @@
""""Config flow for Lupusec integration."""
from json import JSONDecodeError
import logging
from typing import Any
@@ -50,6 +51,8 @@ class LupusecConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
await test_host_connection(self.hass, host, username, password)
except CannotConnect:
errors["base"] = "cannot_connect"
except JSONDecodeError:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
@@ -80,6 +83,8 @@ class LupusecConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
await test_host_connection(self.hass, host, username, password)
except CannotConnect:
return self.async_abort(reason="cannot_connect")
except JSONDecodeError:
return self.async_abort(reason="cannot_connect")
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
return self.async_abort(reason="unknown")
@@ -21,7 +21,7 @@
"issues": {
"deprecated_yaml_import_issue_cannot_connect": {
"title": "The Lupus Electronics LUPUSEC YAML configuration import failed",
"description": "Configuring Lupus Electronics LUPUSEC using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to Lupus Electronics LUPUSEC works and restart Home Assistant to try again or remove the Lupus Electronics LUPUSEC YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
"description": "Configuring Lupus Electronics LUPUSEC using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to Lupus Electronics LUPUSEC works and restart Home Assistant to try again or remove the Lupus Electronics LUPUSEC YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
},
"deprecated_yaml_import_issue_unknown": {
"title": "The Lupus Electronics LUPUSEC YAML configuration import failed",
+6 -3
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import timedelta
from functools import partial
from typing import Any
import lupupy.constants as CONST
@@ -20,7 +21,7 @@ SCAN_INTERVAL = timedelta(seconds=2)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_devices: AddEntitiesCallback,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Lupusec switch devices."""
@@ -29,10 +30,12 @@ async def async_setup_entry(
device_types = CONST.TYPE_SWITCH
switches = []
for device in data.get_devices(generic_type=device_types):
partial_func = partial(data.get_devices, generic_type=device_types)
devices = await hass.async_add_executor_job(partial_func)
for device in devices:
switches.append(LupusecSwitch(device, config_entry.entry_id))
async_add_devices(switches)
async_add_entities(switches)
class LupusecSwitch(LupusecBaseSensor, SwitchEntity):
@@ -6,5 +6,5 @@
"dependencies": ["websocket_api"],
"documentation": "https://www.home-assistant.io/integrations/matter",
"iot_class": "local_push",
"requirements": ["python-matter-server==5.4.0"]
"requirements": ["python-matter-server==5.4.1"]
}
@@ -22,7 +22,7 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
@@ -123,11 +123,6 @@ class MelCloudDevice:
via_device=(DOMAIN, f"{dev.mac}-{dev.serial}"),
)
@property
def daily_energy_consumed(self) -> float | None:
"""Return energy consumed during the current day in kWh."""
return self.device.daily_energy_consumed
async def mel_devices_setup(
hass: HomeAssistant, token: str
@@ -138,8 +133,8 @@ async def mel_devices_setup(
all_devices = await get_devices(
token,
session,
conf_update_interval=timedelta(minutes=5),
device_set_debounce=timedelta(seconds=1),
conf_update_interval=timedelta(minutes=30),
device_set_debounce=timedelta(seconds=2),
)
wrapped_devices: dict[str, list[MelCloudDevice]] = {}
for device_type, devices in all_devices.items():
@@ -1,10 +1,10 @@
{
"domain": "melcloud",
"name": "MELCloud",
"codeowners": ["@vilppuvuorinen"],
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/melcloud",
"iot_class": "cloud_polling",
"loggers": ["pymelcloud"],
"requirements": ["pymelcloud==2.5.8"]
"requirements": ["pymelcloud==2.5.9"]
}
@@ -58,16 +58,6 @@ ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = (
value_fn=lambda x: x.device.total_energy_consumed,
enabled=lambda x: x.device.has_energy_consumed_meter,
),
MelcloudSensorEntityDescription(
key="daily_energy",
translation_key="daily_energy",
icon="mdi:factory",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda x: x.device.daily_energy_consumed,
enabled=lambda x: True,
),
)
ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = (
MelcloudSensorEntityDescription(
@@ -90,16 +80,6 @@ ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = (
value_fn=lambda x: x.device.tank_temperature,
enabled=lambda x: True,
),
MelcloudSensorEntityDescription(
key="daily_energy",
translation_key="daily_energy",
icon="mdi:factory",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda x: x.device.daily_energy_consumed,
enabled=lambda x: True,
),
)
ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = (
MelcloudSensorEntityDescription(
@@ -65,9 +65,6 @@
"room_temperature": {
"name": "Room temperature"
},
"daily_energy": {
"name": "Daily energy consumed"
},
"outside_temperature": {
"name": "Outside temperature"
},
+7 -1
View File
@@ -1,4 +1,5 @@
"""Support for mill wifi-enabled home heaters."""
from typing import Any
import mill
@@ -186,9 +187,14 @@ class LocalMillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntit
_attr_max_temp = MAX_TEMP
_attr_min_temp = MIN_TEMP
_attr_name = None
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
_attr_target_temperature_step = PRECISION_TENTHS
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_enable_turn_on_off_backwards_compatibility = False
def __init__(self, coordinator: MillDataUpdateCoordinator) -> None:
"""Initialize the thermostat."""
@@ -51,6 +51,7 @@ POSITION_DEVICE_MAP = {
BlindType.CurtainLeft: CoverDeviceClass.CURTAIN,
BlindType.CurtainRight: CoverDeviceClass.CURTAIN,
BlindType.SkylightBlind: CoverDeviceClass.SHADE,
BlindType.InsectScreen: CoverDeviceClass.SHADE,
}
TILT_DEVICE_MAP = {
@@ -69,6 +70,7 @@ TILT_ONLY_DEVICE_MAP = {
TDBU_DEVICE_MAP = {
BlindType.TopDownBottomUp: CoverDeviceClass.SHADE,
BlindType.TriangleBlind: CoverDeviceClass.BLIND,
}
@@ -21,5 +21,5 @@
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
"iot_class": "local_push",
"loggers": ["motionblinds"],
"requirements": ["motionblinds==0.6.19"]
"requirements": ["motionblinds==0.6.20"]
}
@@ -1,5 +1,6 @@
"""Support for Motion Blinds sensors."""
from motionblinds import DEVICE_TYPES_WIFI, BlindType
from motionblinds import DEVICE_TYPES_WIFI
from motionblinds.motion_blinds import DEVICE_TYPE_TDBU
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.config_entries import ConfigEntry
@@ -29,7 +30,7 @@ async def async_setup_entry(
for blind in motion_gateway.device_list.values():
entities.append(MotionSignalStrengthSensor(coordinator, blind))
if blind.type == BlindType.TopDownBottomUp:
if blind.device_type == DEVICE_TYPE_TDBU:
entities.append(MotionTDBUBatterySensor(coordinator, blind, "Bottom"))
entities.append(MotionTDBUBatterySensor(coordinator, blind, "Top"))
elif blind.battery_voltage is not None and blind.battery_voltage > 0:
+1 -1
View File
@@ -75,7 +75,7 @@ class MyUplinkDevicePointSensor(MyUplinkEntity, SensorEntity):
# Internal properties
self.point_id = device_point.parameter_id
self._attr_name = device_point.parameter_name
self._attr_name = device_point.parameter_name.replace("\u002d", "")
if entity_description is not None:
self.entity_description = entity_description
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/nibe_heatpump",
"iot_class": "local_polling",
"requirements": ["nibe==2.7.0"]
"requirements": ["nibe==2.8.0"]
}
+1 -1
View File
@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/otbr",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.5.0"]
"requirements": ["python-otbr-api==2.6.0"]
}
+4 -5
View File
@@ -63,11 +63,6 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
self.gateway_data = coordinator.data.devices[gateway_id]
# Determine supported features
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
if HVACMode.OFF in self.hvac_modes:
self._attr_supported_features |= (
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
)
if (
self.cdr_gateway["cooling_present"]
and self.cdr_gateway["smile_name"] != "Adam"
@@ -75,6 +70,10 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
self._attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
)
if HVACMode.OFF in self.hvac_modes:
self._attr_supported_features |= (
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
)
if presets := self.device.get("preset_modes"):
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
self._attr_preset_modes = presets
+21 -8
View File
@@ -69,6 +69,7 @@ class TrackedEntityDescriptor(NamedTuple):
entity_id: str
identifier: str
name: str
def _device_info(coordinator: ProximityDataUpdateCoordinator) -> DeviceInfo:
@@ -95,13 +96,24 @@ async def async_setup_entry(
entity_reg = er.async_get(hass)
for tracked_entity_id in coordinator.tracked_entities:
tracked_entity_object_id = tracked_entity_id.split(".")[-1]
if (entity_entry := entity_reg.async_get(tracked_entity_id)) is not None:
tracked_entity_descriptors.append(
TrackedEntityDescriptor(tracked_entity_id, entity_entry.id)
TrackedEntityDescriptor(
tracked_entity_id,
entity_entry.id,
entity_entry.name
or entity_entry.original_name
or tracked_entity_object_id,
)
)
else:
tracked_entity_descriptors.append(
TrackedEntityDescriptor(tracked_entity_id, tracked_entity_id)
TrackedEntityDescriptor(
tracked_entity_id,
tracked_entity_id,
tracked_entity_object_id,
)
)
entities += [
@@ -165,7 +177,7 @@ class ProximityTrackedEntitySensor(
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{tracked_entity_descriptor.identifier}_{description.key}"
self._attr_device_info = _device_info(coordinator)
self._attr_translation_placeholders = {
"tracked_entity": self.tracked_entity_id.split(".")[-1]
"tracked_entity": tracked_entity_descriptor.name
}
async def async_added_to_hass(self) -> None:
@@ -176,18 +188,19 @@ class ProximityTrackedEntitySensor(
)
@property
def data(self) -> dict[str, str | int | None] | None:
def data(self) -> dict[str, str | int | None]:
"""Get data from coordinator."""
return self.coordinator.data.entities.get(self.tracked_entity_id)
return self.coordinator.data.entities[self.tracked_entity_id]
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.data is not None
return (
super().available
and self.tracked_entity_id in self.coordinator.data.entities
)
@property
def native_value(self) -> str | float | None:
"""Return native sensor value."""
if self.data is None:
return None
return self.data.get(self.entity_description.key)
@@ -96,7 +96,7 @@ class DiskSpaceDataUpdateCoordinator(RadarrDataUpdateCoordinator[list[RootFolder
"""Fetch the data."""
root_folders = await self.api_client.async_get_root_folders()
if isinstance(root_folders, RootFolder):
root_folders = [root_folders]
return [root_folders]
return root_folders
@@ -105,7 +105,10 @@ class HealthDataUpdateCoordinator(RadarrDataUpdateCoordinator[list[Health]]):
async def _fetch_data(self) -> list[Health]:
"""Fetch the health data."""
return await self.api_client.async_get_failed_health_checks()
health = await self.api_client.async_get_failed_health_checks()
if isinstance(health, Health):
return [health]
return health
class MoviesDataUpdateCoordinator(RadarrDataUpdateCoordinator[int]):
+1 -1
View File
@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/ring",
"iot_class": "cloud_polling",
"loggers": ["ring_doorbell"],
"requirements": ["ring-doorbell[listen]==0.8.5"]
"requirements": ["ring-doorbell[listen]==0.8.7"]
}
@@ -0,0 +1,22 @@
{
"config": {
"flow_title": "[%key:component::bluetooth::config::flow_title%]",
"step": {
"user": {
"description": "[%key:component::bluetooth::config::step::user::description%]",
"data": {
"address": "[%key:common::config_flow::data::device%]"
}
},
"bluetooth_confirm": {
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
}
},
"abort": {
"not_supported": "Device not supported",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}
@@ -156,7 +156,7 @@ WALL_CONNECTOR_SENSORS = [
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda data: data[WALLCONNECTOR_DATA_VITALS].session_energy_wh,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.MEASUREMENT,
state_class=SensorStateClass.TOTAL_INCREASING,
),
WallConnectorSensorDescription(
key="energy_kWh",
@@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/thread",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.5.0", "pyroute2==0.7.5"],
"requirements": ["python-otbr-api==2.6.0", "pyroute2==0.7.5"],
"zeroconf": ["_meshcop._udp.local."]
}
@@ -0,0 +1 @@
"""Virtual integration: TP-Link Tapo."""
@@ -0,0 +1,6 @@
{
"domain": "tplink_tapo",
"name": "Tapo",
"integration_type": "virtual",
"supported_by": "tplink"
}
@@ -78,7 +78,7 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat
self.client.get_geofences(),
)
except TraccarException as ex:
raise UpdateFailed("Error while updating device data: %s") from ex
raise UpdateFailed(f"Error while updating device data: {ex}") from ex
if TYPE_CHECKING:
assert isinstance(devices, list[DeviceModel]) # type: ignore[misc]
@@ -129,6 +129,13 @@ async def _generate_trackables(
if not trackable["device_id"]:
return None
if "details" not in trackable:
_LOGGER.info(
"Tracker %s has no details and will be skipped. This happens for shared trackers",
trackable["device_id"],
)
return None
tracker = client.tracker(trackable["device_id"])
tracker_details, hw_info, pos_report = await asyncio.gather(
+3 -3
View File
@@ -430,10 +430,10 @@ class UnifiSensorEntity(UnifiEntity[HandlerT, ApiItemT], SensorEntity):
def _make_disconnected(self, *_: core_Event) -> None:
"""No heart beat by device.
Reset sensor value to 0 when client device is disconnected
Set sensor as unavailable when client device is disconnected
"""
if self._attr_native_value != 0:
self._attr_native_value = 0
if self._attr_available:
self._attr_available = False
self.async_write_ha_state()
@callback
@@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.41"]
"requirements": ["holidays==0.42"]
}
+12 -2
View File
@@ -19,8 +19,10 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import (
aiohttp_client,
config_entry_oauth2_flow,
config_validation as cv,
device_registry as dr,
)
from homeassistant.helpers.typing import ConfigType
from . import api
from .const import DOMAIN, YOLINK_EVENT
@@ -30,6 +32,8 @@ from .services import async_register_services
SCAN_INTERVAL = timedelta(minutes=5)
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
PLATFORMS = [
Platform.BINARY_SENSOR,
@@ -96,6 +100,14 @@ class YoLinkHomeStore:
device_coordinators: dict[str, YoLinkCoordinator]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up YoLink."""
async_register_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up yolink from a config entry."""
hass.data.setdefault(DOMAIN, {})
@@ -147,8 +159,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async_register_services(hass, entry)
async def async_yolink_unload(event) -> None:
"""Unload yolink."""
await yolink_home.async_unload()
+16 -2
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from yolink.client_request import ClientRequest
from yolink.const import ATTR_DEVICE_SPEAKER_HUB
@@ -30,6 +31,7 @@ class YoLinkNumberTypeConfigEntityDescription(NumberEntityDescription):
"""YoLink NumberEntity description."""
exists_fn: Callable[[YoLinkDevice], bool]
should_update_entity: Callable
value: Callable
@@ -37,6 +39,14 @@ NUMBER_TYPE_CONF_SUPPORT_DEVICES = [ATTR_DEVICE_SPEAKER_HUB]
SUPPORT_SET_VOLUME_DEVICES = [ATTR_DEVICE_SPEAKER_HUB]
def get_volume_value(state: dict[str, Any]) -> int | None:
"""Get volume option."""
if (options := state.get("options")) is not None:
return options.get("volume")
return None
DEVICE_CONFIG_DESCRIPTIONS: tuple[YoLinkNumberTypeConfigEntityDescription, ...] = (
YoLinkNumberTypeConfigEntityDescription(
key=OPTIONS_VALUME,
@@ -48,7 +58,8 @@ DEVICE_CONFIG_DESCRIPTIONS: tuple[YoLinkNumberTypeConfigEntityDescription, ...]
native_unit_of_measurement=None,
icon="mdi:volume-high",
exists_fn=lambda device: device.device_type in SUPPORT_SET_VOLUME_DEVICES,
value=lambda state: state["options"]["volume"],
should_update_entity=lambda value: value is not None,
value=get_volume_value,
),
)
@@ -98,7 +109,10 @@ class YoLinkNumberTypeConfigEntity(YoLinkEntity, NumberEntity):
@callback
def update_entity_state(self, state: dict) -> None:
"""Update HA Entity State."""
attr_val = self.entity_description.value(state)
if (
attr_val := self.entity_description.value(state)
) is None and self.entity_description.should_update_entity(attr_val) is False:
return
self._attr_native_value = attr_val
self.async_write_ha_state()
+14 -2
View File
@@ -3,8 +3,9 @@
import voluptuous as vol
from yolink.client_request import ClientRequest
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
from .const import (
@@ -19,7 +20,7 @@ from .const import (
SERVICE_PLAY_ON_SPEAKER_HUB = "play_on_speaker_hub"
def async_register_services(hass: HomeAssistant, entry: ConfigEntry) -> None:
def async_register_services(hass: HomeAssistant) -> None:
"""Register services for YoLink integration."""
async def handle_speaker_hub_play_call(service_call: ServiceCall) -> None:
@@ -28,6 +29,17 @@ def async_register_services(hass: HomeAssistant, entry: ConfigEntry) -> None:
device_registry = dr.async_get(hass)
device_entry = device_registry.async_get(service_data[ATTR_TARGET_DEVICE])
if device_entry is not None:
for entry_id in device_entry.config_entries:
if (entry := hass.config_entries.async_get_entry(entry_id)) is None:
continue
if entry.domain == DOMAIN:
break
if entry is None or entry.state == ConfigEntryState.NOT_LOADED:
raise ServiceValidationError(
"Config entry not found or not loaded!",
translation_domain=DOMAIN,
translation_key="invalid_config_entry",
)
home_store = hass.data[DOMAIN][entry.entry_id]
for identifier in device_entry.identifiers:
if (
@@ -7,9 +7,7 @@ play_on_speaker_hub:
device:
filter:
- integration: yolink
manufacturer: YoLink
model: SpeakerHub
message:
required: true
example: hello, yolink
@@ -37,6 +37,11 @@
"button_4_long_press": "Button_4 (long press)"
}
},
"exceptions": {
"invalid_config_entry": {
"message": "Config entry not found or not loaded!"
}
},
"entity": {
"switch": {
"usb_ports": { "name": "USB ports" },
+5 -5
View File
@@ -21,16 +21,16 @@
"universal_silabs_flasher"
],
"requirements": [
"bellows==0.37.6",
"bellows==0.38.0",
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.110",
"zigpy-deconz==0.22.4",
"zigpy==0.61.0",
"zha-quirks==0.0.111",
"zigpy-deconz==0.23.0",
"zigpy==0.62.3",
"zigpy-xbee==0.20.1",
"zigpy-zigate==0.12.0",
"zigpy-znp==0.12.1",
"universal-silabs-flasher==0.0.15",
"universal-silabs-flasher==0.0.18",
"pyserial-asyncio-fast==0.11"
],
"usb": [
+20
View File
@@ -838,6 +838,26 @@ class SmartEnergySummationReceived(PolledSmartEnergySummation):
_unique_id_suffix = "summation_received"
_attr_translation_key: str = "summation_received"
@classmethod
def create_entity(
cls,
unique_id: str,
zha_device: ZHADevice,
cluster_handlers: list[ClusterHandler],
**kwargs: Any,
) -> Self | None:
"""Entity Factory.
This attribute only started to be initialized in HA 2024.2.0,
so the entity would still be created on the first HA start after the upgrade for existing devices,
as the initialization to see if an attribute is unsupported happens later in the background.
To avoid creating a lot of unnecessary entities for existing devices,
wait until the attribute was properly initialized once for now.
"""
if cluster_handlers[0].cluster.get(cls._attribute_name) is None:
return None
return super().create_entity(unique_id, zha_device, cluster_handlers, **kwargs)
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_PRESSURE)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
+1 -1
View File
@@ -16,7 +16,7 @@ from .helpers.deprecation import (
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 2
PATCH_VERSION: Final = "0b7"
PATCH_VERSION: Final = "0"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
@@ -6156,6 +6156,12 @@
"config_flow": false,
"iot_class": "local_polling",
"name": "TP-Link LTE"
},
"tplink_tapo": {
"integration_type": "virtual",
"config_flow": false,
"supported_by": "tplink",
"name": "Tapo"
}
},
"iot_standards": [
@@ -209,7 +209,10 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation):
error_code = error_response.get("error", "unknown")
error_description = error_response.get("error_description", "unknown error")
_LOGGER.error(
"Token request failed (%s): %s", error_code, error_description
"Token request for %s failed (%s): %s",
self.domain,
error_code,
error_description,
)
resp.raise_for_status()
return cast(dict, await resp.json())
+23 -3
View File
@@ -57,6 +57,7 @@ SLOW_ADD_MIN_TIMEOUT = 500
PLATFORM_NOT_READY_RETRIES = 10
DATA_ENTITY_PLATFORM = "entity_platform"
DATA_DOMAIN_ENTITIES = "domain_entities"
DATA_DOMAIN_PLATFORM_ENTITIES = "domain_platform_entities"
PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds
_LOGGER = getLogger(__name__)
@@ -124,6 +125,8 @@ class EntityPlatform:
self.scan_interval = scan_interval
self.entity_namespace = entity_namespace
self.config_entry: config_entries.ConfigEntry | None = None
# Storage for entities for this specific platform only
# which are indexed by entity_id
self.entities: dict[str, Entity] = {}
self.component_translations: dict[str, Any] = {}
self.platform_translations: dict[str, Any] = {}
@@ -145,9 +148,24 @@ class EntityPlatform:
# which powers entity_component.add_entities
self.parallel_updates_created = platform is None
self.domain_entities: dict[str, Entity] = hass.data.setdefault(
# Storage for entities indexed by domain
# with the child dict indexed by entity_id
#
# This is usually media_player, light, switch, etc.
domain_entities: dict[str, dict[str, Entity]] = hass.data.setdefault(
DATA_DOMAIN_ENTITIES, {}
).setdefault(domain, {})
)
self.domain_entities = domain_entities.setdefault(domain, {})
# Storage for entities indexed by domain and platform
# with the child dict indexed by entity_id
#
# This is usually media_player.yamaha, light.hue, switch.tplink, etc.
domain_platform_entities: dict[
tuple[str, str], dict[str, Entity]
] = hass.data.setdefault(DATA_DOMAIN_PLATFORM_ENTITIES, {})
key = (domain, platform_name)
self.domain_platform_entities = domain_platform_entities.setdefault(key, {})
def __repr__(self) -> str:
"""Represent an EntityPlatform."""
@@ -743,6 +761,7 @@ class EntityPlatform:
entity_id = entity.entity_id
self.entities[entity_id] = entity
self.domain_entities[entity_id] = entity
self.domain_platform_entities[entity_id] = entity
if not restored:
# Reserve the state in the state machine
@@ -756,6 +775,7 @@ class EntityPlatform:
"""Remove entity from entities dict."""
self.entities.pop(entity_id)
self.domain_entities.pop(entity_id)
self.domain_platform_entities.pop(entity_id)
entity.async_on_remove(remove_entity_cb)
@@ -852,7 +872,7 @@ class EntityPlatform:
partial(
service.entity_service_call,
self.hass,
self.domain_entities,
self.domain_platform_entities,
service_func,
required_features=required_features,
),
+5 -2
View File
@@ -9,7 +9,7 @@ astral==2.2
async-upnp-client==0.38.1
atomicwrites-homeassistant==1.4.1
attrs==23.2.0
awesomeversion==23.11.0
awesomeversion==24.2.0
bcrypt==4.0.1
bleak-retry-connector==3.4.0
bleak==0.21.1
@@ -28,7 +28,7 @@ habluetooth==2.4.0
hass-nabucasa==0.76.0
hassil==1.6.1
home-assistant-bluetooth==1.12.0
home-assistant-frontend==20240202.0
home-assistant-frontend==20240207.0
home-assistant-intents==2024.2.2
httpx==0.26.0
ifaddr==0.2.0
@@ -188,3 +188,6 @@ dacite>=1.7.0
# Musle wheels for pandas 2.2.0 cannot be build for any architecture.
pandas==2.1.4
# chacha20poly1305-reuseable==0.12.0 is incompatible with cryptography==42.0.x
chacha20poly1305-reuseable>=0.12.1
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2024.2.0b7"
version = "2024.2.0"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
@@ -30,7 +30,7 @@ dependencies = [
"astral==2.2",
"attrs==23.2.0",
"atomicwrites-homeassistant==1.4.1",
"awesomeversion==23.11.0",
"awesomeversion==24.2.0",
"bcrypt==4.0.1",
"certifi>=2021.5.30",
"ciso8601==2.3.0",
+1 -1
View File
@@ -10,7 +10,7 @@ aiohttp-zlib-ng==0.3.1
astral==2.2
attrs==23.2.0
atomicwrites-homeassistant==1.4.1
awesomeversion==23.11.0
awesomeversion==24.2.0
bcrypt==4.0.1
certifi>=2021.5.30
ciso8601==2.3.0
+24 -24
View File
@@ -76,7 +76,7 @@ PyMetEireann==2021.8.0
PyMetno==0.11.0
# homeassistant.components.keymitt_ble
PyMicroBot==0.0.9
PyMicroBot==0.0.10
# homeassistant.components.nina
PyNINA==0.3.3
@@ -170,7 +170,7 @@ agent-py==0.0.23
aio-geojson-generic-client==0.4
# homeassistant.components.geonetnz_quakes
aio-geojson-geonetnz-quakes==0.15
aio-geojson-geonetnz-quakes==0.16
# homeassistant.components.geonetnz_volcano
aio-geojson-geonetnz-volcano==0.8
@@ -182,7 +182,7 @@ aio-geojson-nsw-rfs-incidents==0.7
aio-geojson-usgs-earthquakes==0.2
# homeassistant.components.gdacs
aio-georss-gdacs==0.8
aio-georss-gdacs==0.9
# homeassistant.components.airq
aioairq==0.3.2
@@ -215,7 +215,7 @@ aiobafi6==0.9.0
aiobotocore==2.9.1
# homeassistant.components.comelit
aiocomelit==0.8.2
aiocomelit==0.8.3
# homeassistant.components.dhcp
aiodiscover==1.6.0
@@ -230,16 +230,16 @@ aioeafm==0.1.2
aioeagle==1.1.0
# homeassistant.components.ecowitt
aioecowitt==2023.5.0
aioecowitt==2024.2.0
# homeassistant.components.co2signal
aioelectricitymaps==0.3.0
aioelectricitymaps==0.3.1
# homeassistant.components.emonitor
aioemonitor==1.0.5
# homeassistant.components.esphome
aioesphomeapi==21.0.1
aioesphomeapi==21.0.2
# homeassistant.components.flo
aioflo==2021.11.0
@@ -425,7 +425,7 @@ airtouch5py==0.2.8
alpha-vantage==2.3.1
# homeassistant.components.amberelectric
amberelectric==1.0.4
amberelectric==1.1.0
# homeassistant.components.amcrest
amcrest==1.9.8
@@ -535,7 +535,7 @@ beautifulsoup4==4.12.2
# beewi-smartclim==0.0.10
# homeassistant.components.zha
bellows==0.37.6
bellows==0.38.0
# homeassistant.components.bmw_connected_drive
bimmer-connected[china]==0.14.6
@@ -1056,10 +1056,10 @@ hole==0.8.0
# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.41
holidays==0.42
# homeassistant.components.frontend
home-assistant-frontend==20240202.0
home-assistant-frontend==20240207.0
# homeassistant.components.conversation
home-assistant-intents==2024.2.2
@@ -1310,7 +1310,7 @@ moehlenhoff-alpha2==1.3.0
mopeka-iot-ble==0.5.0
# homeassistant.components.motion_blinds
motionblinds==0.6.19
motionblinds==0.6.20
# homeassistant.components.motioneye
motioneye-client==0.3.14
@@ -1367,7 +1367,7 @@ nextcord==2.0.0a8
nextdns==2.1.0
# homeassistant.components.nibe_heatpump
nibe==2.7.0
nibe==2.8.0
# homeassistant.components.niko_home_control
niko-home-control==0.2.1
@@ -1940,7 +1940,7 @@ pymata-express==1.19
pymediaroom==0.6.5.4
# homeassistant.components.melcloud
pymelcloud==2.5.8
pymelcloud==2.5.9
# homeassistant.components.meteoclimatic
pymeteoclimatic==0.1.0
@@ -2181,7 +2181,7 @@ python-awair==0.2.4
python-blockchain-api==0.0.2
# homeassistant.components.bring
python-bring-api==2.0.0
python-bring-api==3.0.0
# homeassistant.components.bsblan
python-bsblan==0.5.18
@@ -2238,7 +2238,7 @@ python-kasa[speedups]==0.6.2.1
# python-lirc==1.2.3
# homeassistant.components.matter
python-matter-server==5.4.0
python-matter-server==5.4.1
# homeassistant.components.xiaomi_miio
python-miio==0.5.12
@@ -2257,7 +2257,7 @@ python-opensky==1.0.0
# homeassistant.components.otbr
# homeassistant.components.thread
python-otbr-api==2.5.0
python-otbr-api==2.6.0
# homeassistant.components.picnic
python-picnic-api==1.1.0
@@ -2429,7 +2429,7 @@ rfk101py==0.0.1
rflink==0.0.65
# homeassistant.components.ring
ring-doorbell[listen]==0.8.5
ring-doorbell[listen]==0.8.7
# homeassistant.components.fleetgo
ritassist==0.9.2
@@ -2757,7 +2757,7 @@ unifi_ap==0.0.1
unifiled==0.11
# homeassistant.components.zha
universal-silabs-flasher==0.0.15
universal-silabs-flasher==0.0.18
# homeassistant.components.upb
upb-lib==0.5.4
@@ -2856,10 +2856,10 @@ xbox-webapi==2.0.11
xiaomi-ble==0.23.1
# homeassistant.components.knx
xknx==2.11.2
xknx==2.12.0
# homeassistant.components.knx
xknxproject==3.4.0
xknxproject==3.5.0
# homeassistant.components.bluesound
# homeassistant.components.fritz
@@ -2913,7 +2913,7 @@ zeroconf==0.131.0
zeversolar==0.3.1
# homeassistant.components.zha
zha-quirks==0.0.110
zha-quirks==0.0.111
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.9
@@ -2922,7 +2922,7 @@ zhong-hong-hvac==1.0.9
ziggo-mediabox-xl==1.1.0
# homeassistant.components.zha
zigpy-deconz==0.22.4
zigpy-deconz==0.23.0
# homeassistant.components.zha
zigpy-xbee==0.20.1
@@ -2934,7 +2934,7 @@ zigpy-zigate==0.12.0
zigpy-znp==0.12.1
# homeassistant.components.zha
zigpy==0.61.0
zigpy==0.62.3
# homeassistant.components.zoneminder
zm-py==0.5.4
+24 -24
View File
@@ -64,7 +64,7 @@ PyMetEireann==2021.8.0
PyMetno==0.11.0
# homeassistant.components.keymitt_ble
PyMicroBot==0.0.9
PyMicroBot==0.0.10
# homeassistant.components.nina
PyNINA==0.3.3
@@ -149,7 +149,7 @@ agent-py==0.0.23
aio-geojson-generic-client==0.4
# homeassistant.components.geonetnz_quakes
aio-geojson-geonetnz-quakes==0.15
aio-geojson-geonetnz-quakes==0.16
# homeassistant.components.geonetnz_volcano
aio-geojson-geonetnz-volcano==0.8
@@ -161,7 +161,7 @@ aio-geojson-nsw-rfs-incidents==0.7
aio-geojson-usgs-earthquakes==0.2
# homeassistant.components.gdacs
aio-georss-gdacs==0.8
aio-georss-gdacs==0.9
# homeassistant.components.airq
aioairq==0.3.2
@@ -194,7 +194,7 @@ aiobafi6==0.9.0
aiobotocore==2.9.1
# homeassistant.components.comelit
aiocomelit==0.8.2
aiocomelit==0.8.3
# homeassistant.components.dhcp
aiodiscover==1.6.0
@@ -209,16 +209,16 @@ aioeafm==0.1.2
aioeagle==1.1.0
# homeassistant.components.ecowitt
aioecowitt==2023.5.0
aioecowitt==2024.2.0
# homeassistant.components.co2signal
aioelectricitymaps==0.3.0
aioelectricitymaps==0.3.1
# homeassistant.components.emonitor
aioemonitor==1.0.5
# homeassistant.components.esphome
aioesphomeapi==21.0.1
aioesphomeapi==21.0.2
# homeassistant.components.flo
aioflo==2021.11.0
@@ -395,7 +395,7 @@ airtouch4pyapi==1.0.5
airtouch5py==0.2.8
# homeassistant.components.amberelectric
amberelectric==1.0.4
amberelectric==1.1.0
# homeassistant.components.androidtv
androidtv[async]==0.0.73
@@ -457,7 +457,7 @@ base36==0.1.1
beautifulsoup4==4.12.2
# homeassistant.components.zha
bellows==0.37.6
bellows==0.38.0
# homeassistant.components.bmw_connected_drive
bimmer-connected[china]==0.14.6
@@ -852,10 +852,10 @@ hole==0.8.0
# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.41
holidays==0.42
# homeassistant.components.frontend
home-assistant-frontend==20240202.0
home-assistant-frontend==20240207.0
# homeassistant.components.conversation
home-assistant-intents==2024.2.2
@@ -1046,7 +1046,7 @@ moehlenhoff-alpha2==1.3.0
mopeka-iot-ble==0.5.0
# homeassistant.components.motion_blinds
motionblinds==0.6.19
motionblinds==0.6.20
# homeassistant.components.motioneye
motioneye-client==0.3.14
@@ -1094,7 +1094,7 @@ nextcord==2.0.0a8
nextdns==2.1.0
# homeassistant.components.nibe_heatpump
nibe==2.7.0
nibe==2.8.0
# homeassistant.components.nfandroidtv
notifications-android-tv==0.1.5
@@ -1494,7 +1494,7 @@ pymailgunner==1.4
pymata-express==1.19
# homeassistant.components.melcloud
pymelcloud==2.5.8
pymelcloud==2.5.9
# homeassistant.components.meteoclimatic
pymeteoclimatic==0.1.0
@@ -1684,7 +1684,7 @@ python-MotionMount==0.3.1
python-awair==0.2.4
# homeassistant.components.bring
python-bring-api==2.0.0
python-bring-api==3.0.0
# homeassistant.components.bsblan
python-bsblan==0.5.18
@@ -1711,7 +1711,7 @@ python-juicenet==1.1.0
python-kasa[speedups]==0.6.2.1
# homeassistant.components.matter
python-matter-server==5.4.0
python-matter-server==5.4.1
# homeassistant.components.xiaomi_miio
python-miio==0.5.12
@@ -1727,7 +1727,7 @@ python-opensky==1.0.0
# homeassistant.components.otbr
# homeassistant.components.thread
python-otbr-api==2.5.0
python-otbr-api==2.6.0
# homeassistant.components.picnic
python-picnic-api==1.1.0
@@ -1860,7 +1860,7 @@ reolink-aio==0.8.7
rflink==0.0.65
# homeassistant.components.ring
ring-doorbell[listen]==0.8.5
ring-doorbell[listen]==0.8.7
# homeassistant.components.roku
rokuecp==0.19.0
@@ -2098,7 +2098,7 @@ ultraheat-api==0.5.7
unifi-discovery==1.1.7
# homeassistant.components.zha
universal-silabs-flasher==0.0.15
universal-silabs-flasher==0.0.18
# homeassistant.components.upb
upb-lib==0.5.4
@@ -2185,10 +2185,10 @@ xbox-webapi==2.0.11
xiaomi-ble==0.23.1
# homeassistant.components.knx
xknx==2.11.2
xknx==2.12.0
# homeassistant.components.knx
xknxproject==3.4.0
xknxproject==3.5.0
# homeassistant.components.bluesound
# homeassistant.components.fritz
@@ -2233,10 +2233,10 @@ zeroconf==0.131.0
zeversolar==0.3.1
# homeassistant.components.zha
zha-quirks==0.0.110
zha-quirks==0.0.111
# homeassistant.components.zha
zigpy-deconz==0.22.4
zigpy-deconz==0.23.0
# homeassistant.components.zha
zigpy-xbee==0.20.1
@@ -2248,7 +2248,7 @@ zigpy-zigate==0.12.0
zigpy-znp==0.12.1
# homeassistant.components.zha
zigpy==0.61.0
zigpy==0.62.3
# homeassistant.components.zwave_js
zwave-js-server-python==0.55.3
+3
View File
@@ -181,6 +181,9 @@ dacite>=1.7.0
# Musle wheels for pandas 2.2.0 cannot be build for any architecture.
pandas==2.1.4
# chacha20poly1305-reuseable==0.12.0 is incompatible with cryptography==42.0.x
chacha20poly1305-reuseable>=0.12.1
"""
GENERATED_MESSAGE = (
@@ -1,17 +1,18 @@
"""Tests for the Amber config flow."""
from collections.abc import Generator
from datetime import date
from unittest.mock import Mock, patch
from amberelectric import ApiException
from amberelectric.model.site import Site
from amberelectric.model.site import Site, SiteStatus
import pytest
from homeassistant import data_entry_flow
from homeassistant.components.amberelectric.config_flow import filter_sites
from homeassistant.components.amberelectric.const import (
CONF_SITE_ID,
CONF_SITE_NAME,
CONF_SITE_NMI,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER
@@ -26,29 +27,88 @@ pytestmark = pytest.mark.usefixtures("mock_setup_entry")
@pytest.fixture(name="invalid_key_api")
def mock_invalid_key_api() -> Generator:
"""Return an authentication error."""
instance = Mock()
instance.get_sites.side_effect = ApiException(status=403)
with patch("amberelectric.api.AmberApi.create", return_value=instance):
yield instance
with patch("amberelectric.api.AmberApi.create") as mock:
mock.return_value.get_sites.side_effect = ApiException(status=403)
yield mock
@pytest.fixture(name="api_error")
def mock_api_error() -> Generator:
"""Return an authentication error."""
instance = Mock()
instance.get_sites.side_effect = ApiException(status=500)
with patch("amberelectric.api.AmberApi.create", return_value=instance):
yield instance
with patch("amberelectric.api.AmberApi.create") as mock:
mock.return_value.get_sites.side_effect = ApiException(status=500)
yield mock
@pytest.fixture(name="single_site_api")
def mock_single_site_api() -> Generator:
"""Return a single site."""
site = Site(
"01FG0AGP818PXK0DWHXJRRT2DH",
"11111111111",
[],
"Jemena",
SiteStatus.ACTIVE,
date(2002, 1, 1),
None,
)
with patch("amberelectric.api.AmberApi.create") as mock:
mock.return_value.get_sites.return_value = [site]
yield mock
@pytest.fixture(name="single_site_pending_api")
def mock_single_site_pending_api() -> Generator:
"""Return a single site."""
site = Site(
"01FG0AGP818PXK0DWHXJRRT2DH",
"11111111111",
[],
"Jemena",
SiteStatus.PENDING,
None,
None,
)
with patch("amberelectric.api.AmberApi.create") as mock:
mock.return_value.get_sites.return_value = [site]
yield mock
@pytest.fixture(name="single_site_rejoin_api")
def mock_single_site_rejoin_api() -> Generator:
"""Return a single site."""
instance = Mock()
site = Site("01FG0AGP818PXK0DWHXJRRT2DH", "11111111111", [])
instance.get_sites.return_value = [site]
site_1 = Site(
"01HGD9QB72HB3DWQNJ6SSCGXGV",
"11111111111",
[],
"Jemena",
SiteStatus.CLOSED,
date(2002, 1, 1),
date(2002, 6, 1),
)
site_2 = Site(
"01FG0AGP818PXK0DWHXJRRT2DH",
"11111111111",
[],
"Jemena",
SiteStatus.ACTIVE,
date(2003, 1, 1),
None,
)
site_3 = Site(
"01FG0AGP818PXK0DWHXJRRT2DH",
"11111111112",
[],
"Jemena",
SiteStatus.CLOSED,
date(2003, 1, 1),
date(2003, 6, 1),
)
instance.get_sites.return_value = [site_1, site_2, site_3]
with patch("amberelectric.api.AmberApi.create", return_value=instance):
yield instance
@@ -64,6 +124,39 @@ def mock_no_site_api() -> Generator:
yield instance
async def test_single_pending_site(
hass: HomeAssistant, single_site_pending_api: Mock
) -> None:
"""Test single site."""
initial_result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert initial_result.get("type") == data_entry_flow.FlowResultType.FORM
assert initial_result.get("step_id") == "user"
# Test filling in API key
enter_api_key_result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
data={CONF_API_TOKEN: API_KEY},
)
assert enter_api_key_result.get("type") == data_entry_flow.FlowResultType.FORM
assert enter_api_key_result.get("step_id") == "site"
select_site_result = await hass.config_entries.flow.async_configure(
enter_api_key_result["flow_id"],
{CONF_SITE_ID: "01FG0AGP818PXK0DWHXJRRT2DH", CONF_SITE_NAME: "Home"},
)
# Show available sites
assert select_site_result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY
assert select_site_result.get("title") == "Home"
data = select_site_result.get("data")
assert data
assert data[CONF_API_TOKEN] == API_KEY
assert data[CONF_SITE_ID] == "01FG0AGP818PXK0DWHXJRRT2DH"
async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None:
"""Test single site."""
initial_result = await hass.config_entries.flow.async_init(
@@ -83,7 +176,40 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None:
select_site_result = await hass.config_entries.flow.async_configure(
enter_api_key_result["flow_id"],
{CONF_SITE_NMI: "11111111111", CONF_SITE_NAME: "Home"},
{CONF_SITE_ID: "01FG0AGP818PXK0DWHXJRRT2DH", CONF_SITE_NAME: "Home"},
)
# Show available sites
assert select_site_result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY
assert select_site_result.get("title") == "Home"
data = select_site_result.get("data")
assert data
assert data[CONF_API_TOKEN] == API_KEY
assert data[CONF_SITE_ID] == "01FG0AGP818PXK0DWHXJRRT2DH"
async def test_single_site_rejoin(
hass: HomeAssistant, single_site_rejoin_api: Mock
) -> None:
"""Test single site."""
initial_result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert initial_result.get("type") == data_entry_flow.FlowResultType.FORM
assert initial_result.get("step_id") == "user"
# Test filling in API key
enter_api_key_result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
data={CONF_API_TOKEN: API_KEY},
)
assert enter_api_key_result.get("type") == data_entry_flow.FlowResultType.FORM
assert enter_api_key_result.get("step_id") == "site"
select_site_result = await hass.config_entries.flow.async_configure(
enter_api_key_result["flow_id"],
{CONF_SITE_ID: "01FG0AGP818PXK0DWHXJRRT2DH", CONF_SITE_NAME: "Home"},
)
# Show available sites
@@ -93,7 +219,6 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None:
assert data
assert data[CONF_API_TOKEN] == API_KEY
assert data[CONF_SITE_ID] == "01FG0AGP818PXK0DWHXJRRT2DH"
assert data[CONF_SITE_NMI] == "11111111111"
async def test_no_site(hass: HomeAssistant, no_site_api: Mock) -> None:
@@ -148,3 +273,15 @@ async def test_unknown_error(hass: HomeAssistant, api_error: Mock) -> None:
# Goes back to the user step
assert result.get("step_id") == "user"
assert result.get("errors") == {"api_token": "unknown_error"}
async def test_site_deduplication(single_site_rejoin_api: Mock) -> None:
"""Test site deduplication."""
filtered = filter_sites(single_site_rejoin_api.get_sites())
assert len(filtered) == 2
assert (
next(s for s in filtered if s.nmi == "11111111111").status == SiteStatus.ACTIVE
)
assert (
next(s for s in filtered if s.nmi == "11111111112").status == SiteStatus.CLOSED
)
@@ -2,13 +2,14 @@
from __future__ import annotations
from collections.abc import Generator
from datetime import date
from unittest.mock import Mock, patch
from amberelectric import ApiException
from amberelectric.model.channel import Channel, ChannelType
from amberelectric.model.current_interval import CurrentInterval
from amberelectric.model.interval import Descriptor, SpikeStatus
from amberelectric.model.site import Site
from amberelectric.model.site import Site, SiteStatus
from dateutil import parser
import pytest
@@ -38,23 +39,35 @@ def mock_api_current_price() -> Generator:
general_site = Site(
GENERAL_ONLY_SITE_ID,
"11111111111",
[Channel(identifier="E1", type=ChannelType.GENERAL)],
[Channel(identifier="E1", type=ChannelType.GENERAL, tariff="A100")],
"Jemena",
SiteStatus.ACTIVE,
date(2021, 1, 1),
None,
)
general_and_controlled_load = Site(
GENERAL_AND_CONTROLLED_SITE_ID,
"11111111112",
[
Channel(identifier="E1", type=ChannelType.GENERAL),
Channel(identifier="E2", type=ChannelType.CONTROLLED_LOAD),
Channel(identifier="E1", type=ChannelType.GENERAL, tariff="A100"),
Channel(identifier="E2", type=ChannelType.CONTROLLED_LOAD, tariff="A180"),
],
"Jemena",
SiteStatus.ACTIVE,
date(2021, 1, 1),
None,
)
general_and_feed_in = Site(
GENERAL_AND_FEED_IN_SITE_ID,
"11111111113",
[
Channel(identifier="E1", type=ChannelType.GENERAL),
Channel(identifier="E2", type=ChannelType.FEED_IN),
Channel(identifier="E1", type=ChannelType.GENERAL, tariff="A100"),
Channel(identifier="E2", type=ChannelType.FEED_IN, tariff="A100"),
],
"Jemena",
SiteStatus.ACTIVE,
date(2021, 1, 1),
None,
)
instance.get_sites.return_value = [
general_site,
@@ -12,7 +12,7 @@
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.homeassistant_analytics_hacs_custom',
'has_entity_name': True,
'hidden_by': None,
@@ -59,7 +59,7 @@
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.homeassistant_analytics_myq',
'has_entity_name': True,
'hidden_by': None,
@@ -106,7 +106,7 @@
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.homeassistant_analytics_spotify',
'has_entity_name': True,
'hidden_by': None,
@@ -153,7 +153,7 @@
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.homeassistant_analytics_youtube',
'has_entity_name': True,
'hidden_by': None,
@@ -1,6 +1,8 @@
"""Test the Homeassistant Analytics config flow."""
from typing import Any
from unittest.mock import AsyncMock
import pytest
from python_homeassistant_analytics import HomeassistantAnalyticsConnectionError
from homeassistant import config_entries
@@ -16,8 +18,45 @@ from tests.common import MockConfigEntry
from tests.components.analytics_insights import setup_integration
@pytest.mark.parametrize(
("user_input", "expected_options"),
[
(
{
CONF_TRACKED_INTEGRATIONS: ["youtube"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
{
CONF_TRACKED_INTEGRATIONS: ["youtube"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
),
(
{
CONF_TRACKED_INTEGRATIONS: ["youtube"],
},
{
CONF_TRACKED_INTEGRATIONS: ["youtube"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: [],
},
),
(
{
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
{
CONF_TRACKED_INTEGRATIONS: [],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
),
],
)
async def test_form(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_analytics_client: AsyncMock
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_analytics_client: AsyncMock,
user_input: dict[str, Any],
expected_options: dict[str, Any],
) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
@@ -25,6 +64,50 @@ async def test_form(
)
assert result["type"] == FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input,
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "Home Assistant Analytics Insights"
assert result["data"] == {}
assert result["options"] == expected_options
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.parametrize(
"user_input",
[
{
CONF_TRACKED_INTEGRATIONS: [],
CONF_TRACKED_CUSTOM_INTEGRATIONS: [],
},
{},
],
)
async def test_submitting_empty_form(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_analytics_client: AsyncMock,
user_input: dict[str, Any],
) -> None:
"""Test we can't submit an empty form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input,
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert result["errors"] == {"base": "no_integrations_selected"}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
@@ -81,10 +164,45 @@ async def test_form_already_configured(
assert result["reason"] == "already_configured"
@pytest.mark.parametrize(
("user_input", "expected_options"),
[
(
{
CONF_TRACKED_INTEGRATIONS: ["youtube"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
{
CONF_TRACKED_INTEGRATIONS: ["youtube"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
),
(
{
CONF_TRACKED_INTEGRATIONS: ["youtube"],
},
{
CONF_TRACKED_INTEGRATIONS: ["youtube"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: [],
},
),
(
{
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
{
CONF_TRACKED_INTEGRATIONS: [],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
),
],
)
async def test_options_flow(
hass: HomeAssistant,
mock_analytics_client: AsyncMock,
mock_config_entry: MockConfigEntry,
user_input: dict[str, Any],
expected_options: dict[str, Any],
) -> None:
"""Test options flow."""
await setup_integration(hass, mock_config_entry)
@@ -95,7 +213,50 @@ async def test_options_flow(
mock_analytics_client.get_integrations.reset_mock()
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
user_input,
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == expected_options
await hass.async_block_till_done()
mock_analytics_client.get_integrations.assert_called_once()
@pytest.mark.parametrize(
"user_input",
[
{
CONF_TRACKED_INTEGRATIONS: [],
CONF_TRACKED_CUSTOM_INTEGRATIONS: [],
},
{},
],
)
async def test_submitting_empty_options_flow(
hass: HomeAssistant,
mock_analytics_client: AsyncMock,
mock_config_entry: MockConfigEntry,
user_input: dict[str, Any],
) -> None:
"""Test options flow."""
await setup_integration(hass, mock_config_entry)
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input,
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert result["errors"] == {"base": "no_integrations_selected"}
result = await hass.config_entries.options.async_configure(
result["flow_id"],
{
CONF_TRACKED_INTEGRATIONS: ["youtube", "hue"],
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
},
@@ -108,7 +269,6 @@ async def test_options_flow(
CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"],
}
await hass.async_block_till_done()
mock_analytics_client.get_integrations.assert_called_once()
async def test_options_flow_cannot_connect(
+5 -5
View File
@@ -1,6 +1,6 @@
"""Common fixtures for the Bring! tests."""
from collections.abc import Generator
from unittest.mock import Mock, patch
from unittest.mock import AsyncMock, patch
import pytest
@@ -16,7 +16,7 @@ UUID = "00000000-00000000-00000000-00000000"
@pytest.fixture
def mock_setup_entry() -> Generator[Mock, None, None]:
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.bring.async_setup_entry", return_value=True
@@ -25,7 +25,7 @@ def mock_setup_entry() -> Generator[Mock, None, None]:
@pytest.fixture
def mock_bring_client() -> Generator[Mock, None, None]:
def mock_bring_client() -> Generator[AsyncMock, None, None]:
"""Mock a Bring client."""
with patch(
"homeassistant.components.bring.Bring",
@@ -36,8 +36,8 @@ def mock_bring_client() -> Generator[Mock, None, None]:
):
client = mock_client.return_value
client.uuid = UUID
client.login.return_value = True
client.loadLists.return_value = {"lists": []}
client.loginAsync.return_value = True
client.loadListsAsync.return_value = {"lists": []}
yield client
+8 -6
View File
@@ -1,5 +1,5 @@
"""Test the Bring! config flow."""
from unittest.mock import AsyncMock, Mock
from unittest.mock import AsyncMock
import pytest
from python_bring_api.exceptions import (
@@ -25,7 +25,7 @@ MOCK_DATA_STEP = {
async def test_form(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_bring_client: Mock
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_bring_client: AsyncMock
) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
@@ -59,10 +59,10 @@ async def test_form(
],
)
async def test_flow_user_init_data_unknown_error_and_recover(
hass: HomeAssistant, mock_bring_client: Mock, raise_error, text_error
hass: HomeAssistant, mock_bring_client: AsyncMock, raise_error, text_error
) -> None:
"""Test unknown errors."""
mock_bring_client.login.side_effect = raise_error
mock_bring_client.loginAsync.side_effect = raise_error
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}
@@ -76,7 +76,7 @@ async def test_flow_user_init_data_unknown_error_and_recover(
assert result["errors"]["base"] == text_error
# Recover
mock_bring_client.login.side_effect = None
mock_bring_client.loginAsync.side_effect = None
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}
)
@@ -92,7 +92,9 @@ async def test_flow_user_init_data_unknown_error_and_recover(
async def test_flow_user_init_data_already_configured(
hass: HomeAssistant, mock_bring_client: Mock, bring_config_entry: MockConfigEntry
hass: HomeAssistant,
mock_bring_client: AsyncMock,
bring_config_entry: MockConfigEntry,
) -> None:
"""Test we abort user data set when entry is already configured."""
+4 -4
View File
@@ -1,5 +1,5 @@
"""Unit tests for the bring integration."""
from unittest.mock import Mock
from unittest.mock import AsyncMock
import pytest
@@ -27,7 +27,7 @@ async def setup_integration(
async def test_load_unload(
hass: HomeAssistant,
mock_bring_client: Mock,
mock_bring_client: AsyncMock,
bring_config_entry: MockConfigEntry,
) -> None:
"""Test loading and unloading of the config entry."""
@@ -52,12 +52,12 @@ async def test_load_unload(
)
async def test_init_failure(
hass: HomeAssistant,
mock_bring_client: Mock,
mock_bring_client: AsyncMock,
status: ConfigEntryState,
exception: Exception,
bring_config_entry: MockConfigEntry | None,
) -> None:
"""Test an initialization error on integration load."""
mock_bring_client.login.side_effect = exception
mock_bring_client.loginAsync.side_effect = exception
await setup_integration(hass, bring_config_entry)
assert bring_config_entry.state == status
+27 -3
View File
@@ -70,15 +70,20 @@ async def help_setup_mock_config_entry(
@respx.mock
async def test_fetching_url(
hass: HomeAssistant, hass_client: ClientSessionGenerator, fakeimgbytes_png
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
fakeimgbytes_png,
caplog: pytest.CaptureFixture,
) -> None:
"""Test that it fetches the given url."""
respx.get("http://example.com").respond(stream=fakeimgbytes_png)
hass.states.async_set("sensor.temp", "http://example.com/0a")
respx.get("http://example.com/0a").respond(stream=fakeimgbytes_png)
respx.get("http://example.com/1a").respond(stream=fakeimgbytes_png)
options = {
"name": "config_test",
"platform": "generic",
"still_image_url": "http://example.com",
"still_image_url": "{{ states.sensor.temp.state }}",
"username": "user",
"password": "pass",
"authentication": "basic",
@@ -101,6 +106,25 @@ async def test_fetching_url(
resp = await client.get("/api/camera_proxy/camera.config_test")
assert respx.calls.call_count == 2
# If the template renders to an invalid URL we return the last image from cache
hass.states.async_set("sensor.temp", "invalid url")
# sleep another .1 seconds to make cached image expire
await asyncio.sleep(0.1)
resp = await client.get("/api/camera_proxy/camera.config_test")
assert resp.status == HTTPStatus.OK
assert respx.calls.call_count == 2
assert (
"Invalid URL 'invalid url': expected a URL, returning last image" in caplog.text
)
# Restore a valid URL
hass.states.async_set("sensor.temp", "http://example.com/1a")
await asyncio.sleep(0.1)
resp = await client.get("/api/camera_proxy/camera.config_test")
assert resp.status == HTTPStatus.OK
assert respx.calls.call_count == 3
@respx.mock
async def test_image_caching(
+32 -28
View File
@@ -245,7 +245,7 @@ async def test_setup_api_ping(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 20
assert aioclient_mock.call_count == 19
assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0"
assert hass.components.hassio.is_hassio()
@@ -290,7 +290,7 @@ async def test_setup_api_push_api_data(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 20
assert aioclient_mock.call_count == 19
assert not aioclient_mock.mock_calls[1][2]["ssl"]
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
assert aioclient_mock.mock_calls[1][2]["watchdog"]
@@ -309,7 +309,7 @@ async def test_setup_api_push_api_data_server_host(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 20
assert aioclient_mock.call_count == 19
assert not aioclient_mock.mock_calls[1][2]["ssl"]
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
assert not aioclient_mock.mock_calls[1][2]["watchdog"]
@@ -326,7 +326,7 @@ async def test_setup_api_push_api_data_default(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 20
assert aioclient_mock.call_count == 19
assert not aioclient_mock.mock_calls[1][2]["ssl"]
assert aioclient_mock.mock_calls[1][2]["port"] == 8123
refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"]
@@ -406,7 +406,7 @@ async def test_setup_api_existing_hassio_user(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 20
assert aioclient_mock.call_count == 19
assert not aioclient_mock.mock_calls[1][2]["ssl"]
assert aioclient_mock.mock_calls[1][2]["port"] == 8123
assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token
@@ -423,7 +423,7 @@ async def test_setup_core_push_timezone(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 20
assert aioclient_mock.call_count == 19
assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone"
with patch("homeassistant.util.dt.set_default_time_zone"):
@@ -443,7 +443,7 @@ async def test_setup_hassio_no_additional_data(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 20
assert aioclient_mock.call_count == 19
assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456"
@@ -525,14 +525,14 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 24
assert aioclient_mock.call_count == 23
assert aioclient_mock.mock_calls[-1][2] == "test"
await hass.services.async_call("hassio", "host_shutdown", {})
await hass.services.async_call("hassio", "host_reboot", {})
await hass.async_block_till_done()
assert aioclient_mock.call_count == 26
assert aioclient_mock.call_count == 25
await hass.services.async_call("hassio", "backup_full", {})
await hass.services.async_call(
@@ -547,7 +547,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 28
assert aioclient_mock.call_count == 27
assert aioclient_mock.mock_calls[-1][2] == {
"name": "2021-11-13 03:48:00",
"homeassistant": True,
@@ -572,7 +572,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 30
assert aioclient_mock.call_count == 29
assert aioclient_mock.mock_calls[-1][2] == {
"addons": ["test"],
"folders": ["ssl"],
@@ -591,7 +591,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 31
assert aioclient_mock.call_count == 30
assert aioclient_mock.mock_calls[-1][2] == {
"name": "backup_name",
"location": "backup_share",
@@ -607,7 +607,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 32
assert aioclient_mock.call_count == 31
assert aioclient_mock.mock_calls[-1][2] == {
"name": "2021-11-13 03:48:00",
"location": None,
@@ -625,7 +625,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 34
assert aioclient_mock.call_count == 33
assert aioclient_mock.mock_calls[-1][2] == {
"name": "2021-11-13 11:48:00",
"location": None,
@@ -702,12 +702,12 @@ async def test_service_calls_core(
await hass.services.async_call("homeassistant", "stop")
await hass.async_block_till_done()
assert aioclient_mock.call_count == 6
assert aioclient_mock.call_count == 5
await hass.services.async_call("homeassistant", "check_config")
await hass.async_block_till_done()
assert aioclient_mock.call_count == 6
assert aioclient_mock.call_count == 5
with patch(
"homeassistant.config.async_check_ha_config_file", return_value=None
@@ -716,7 +716,7 @@ async def test_service_calls_core(
await hass.async_block_till_done()
assert mock_check_config.called
assert aioclient_mock.call_count == 7
assert aioclient_mock.call_count == 6
async def test_entry_load_and_unload(hass: HomeAssistant) -> None:
@@ -897,14 +897,17 @@ async def test_coordinator_updates(
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# Initial refresh without stats
assert refresh_updates_mock.call_count == 1
# Initial refresh, no update refresh call
assert refresh_updates_mock.call_count == 0
with patch(
"homeassistant.components.hassio.HassIO.refresh_updates",
) as refresh_updates_mock:
async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20))
await hass.async_block_till_done()
# Scheduled refresh, no update refresh call
assert refresh_updates_mock.call_count == 0
with patch(
@@ -921,13 +924,14 @@ async def test_coordinator_updates(
},
blocking=True,
)
assert refresh_updates_mock.call_count == 0
# There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
async_fire_time_changed(
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
)
await hass.async_block_till_done()
# There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
assert refresh_updates_mock.call_count == 0
async_fire_time_changed(
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
)
await hass.async_block_till_done()
assert refresh_updates_mock.call_count == 1
with patch(
"homeassistant.components.hassio.HassIO.refresh_updates",
@@ -968,14 +972,14 @@ async def test_coordinator_updates_stats_entities_enabled(
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# Initial refresh without stats
assert refresh_updates_mock.call_count == 1
assert refresh_updates_mock.call_count == 0
# Refresh with stats once we know which ones are needed
async_fire_time_changed(
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
)
await hass.async_block_till_done()
assert refresh_updates_mock.call_count == 2
assert refresh_updates_mock.call_count == 1
with patch(
"homeassistant.components.hassio.HassIO.refresh_updates",
@@ -1059,7 +1063,7 @@ async def test_setup_hardware_integration(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 20
assert aioclient_mock.call_count == 19
assert len(mock_setup_entry.mock_calls) == 1
+38 -9
View File
@@ -102,6 +102,7 @@ async def test_demo_statistics_growth(
assert statistics[statistic_id][0]["sum"] <= (2**20 + 24)
@pytest.mark.freeze_time("2023-10-21")
async def test_issues_created(
mock_history,
hass: HomeAssistant,
@@ -125,7 +126,7 @@ async def test_issues_created(
"issues": [
{
"breaks_in_ha_version": "2023.1.1",
"created": ANY,
"created": "2023-10-21T00:00:00+00:00",
"dismissed_version": None,
"domain": DOMAIN,
"ignored": False,
@@ -139,7 +140,7 @@ async def test_issues_created(
},
{
"breaks_in_ha_version": "2023.1.1",
"created": ANY,
"created": "2023-10-21T00:00:00+00:00",
"dismissed_version": None,
"domain": DOMAIN,
"ignored": False,
@@ -153,7 +154,7 @@ async def test_issues_created(
},
{
"breaks_in_ha_version": None,
"created": ANY,
"created": "2023-10-21T00:00:00+00:00",
"dismissed_version": None,
"domain": DOMAIN,
"ignored": False,
@@ -167,7 +168,7 @@ async def test_issues_created(
},
{
"breaks_in_ha_version": None,
"created": ANY,
"created": "2023-10-21T00:00:00+00:00",
"dismissed_version": None,
"domain": DOMAIN,
"ignored": False,
@@ -181,7 +182,7 @@ async def test_issues_created(
},
{
"breaks_in_ha_version": None,
"created": ANY,
"created": "2023-10-21T00:00:00+00:00",
"dismissed_version": None,
"domain": DOMAIN,
"is_fixable": True,
@@ -193,6 +194,20 @@ async def test_issues_created(
"translation_placeholders": None,
"ignored": False,
},
{
"breaks_in_ha_version": None,
"created": "2023-10-21T00:00:00+00:00",
"dismissed_version": None,
"domain": "homeassistant",
"is_fixable": False,
"issue_domain": DOMAIN,
"issue_id": ANY,
"learn_more_url": None,
"severity": "error",
"translation_key": "config_entry_reauth",
"translation_placeholders": None,
"ignored": False,
},
]
}
@@ -242,7 +257,7 @@ async def test_issues_created(
"issues": [
{
"breaks_in_ha_version": "2023.1.1",
"created": ANY,
"created": "2023-10-21T00:00:00+00:00",
"dismissed_version": None,
"domain": DOMAIN,
"ignored": False,
@@ -256,7 +271,7 @@ async def test_issues_created(
},
{
"breaks_in_ha_version": None,
"created": ANY,
"created": "2023-10-21T00:00:00+00:00",
"dismissed_version": None,
"domain": DOMAIN,
"ignored": False,
@@ -270,7 +285,7 @@ async def test_issues_created(
},
{
"breaks_in_ha_version": None,
"created": ANY,
"created": "2023-10-21T00:00:00+00:00",
"dismissed_version": None,
"domain": DOMAIN,
"ignored": False,
@@ -284,7 +299,7 @@ async def test_issues_created(
},
{
"breaks_in_ha_version": None,
"created": ANY,
"created": "2023-10-21T00:00:00+00:00",
"dismissed_version": None,
"domain": DOMAIN,
"is_fixable": True,
@@ -296,5 +311,19 @@ async def test_issues_created(
"translation_placeholders": None,
"ignored": False,
},
{
"breaks_in_ha_version": None,
"created": "2023-10-21T00:00:00+00:00",
"dismissed_version": None,
"domain": "homeassistant",
"is_fixable": False,
"issue_domain": DOMAIN,
"issue_id": ANY,
"learn_more_url": None,
"severity": "error",
"translation_key": "config_entry_reauth",
"translation_placeholders": None,
"ignored": False,
},
]
}
+8 -8
View File
@@ -1,5 +1,6 @@
""""Unit tests for the Lupusec config flow."""
from json import JSONDecodeError
from unittest.mock import patch
from lupupy import LupusecException
@@ -51,8 +52,7 @@ async def test_form_valid_input(hass: HomeAssistant) -> None:
"homeassistant.components.lupusec.async_setup_entry",
return_value=True,
) as mock_setup_entry, patch(
"homeassistant.components.lupusec.config_flow.lupupy.Lupusec.__init__",
return_value=None,
"homeassistant.components.lupusec.config_flow.lupupy.Lupusec",
) as mock_initialize_lupusec:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -71,6 +71,7 @@ async def test_form_valid_input(hass: HomeAssistant) -> None:
("raise_error", "text_error"),
[
(LupusecException("Test lupusec exception"), "cannot_connect"),
(JSONDecodeError("Test JSONDecodeError", "test", 1), "cannot_connect"),
(Exception("Test unknown exception"), "unknown"),
],
)
@@ -85,7 +86,7 @@ async def test_flow_user_init_data_error_and_recover(
assert result["errors"] == {}
with patch(
"homeassistant.components.lupusec.config_flow.lupupy.Lupusec.__init__",
"homeassistant.components.lupusec.config_flow.lupupy.Lupusec",
side_effect=raise_error,
) as mock_initialize_lupusec:
result2 = await hass.config_entries.flow.async_configure(
@@ -104,8 +105,7 @@ async def test_flow_user_init_data_error_and_recover(
"homeassistant.components.lupusec.async_setup_entry",
return_value=True,
) as mock_setup_entry, patch(
"homeassistant.components.lupusec.config_flow.lupupy.Lupusec.__init__",
return_value=None,
"homeassistant.components.lupusec.config_flow.lupupy.Lupusec",
) as mock_initialize_lupusec:
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -164,8 +164,7 @@ async def test_flow_source_import(
"homeassistant.components.lupusec.async_setup_entry",
return_value=True,
) as mock_setup_entry, patch(
"homeassistant.components.lupusec.config_flow.lupupy.Lupusec.__init__",
return_value=None,
"homeassistant.components.lupusec.config_flow.lupupy.Lupusec",
) as mock_initialize_lupusec:
result = await hass.config_entries.flow.async_init(
DOMAIN,
@@ -186,6 +185,7 @@ async def test_flow_source_import(
("raise_error", "text_error"),
[
(LupusecException("Test lupusec exception"), "cannot_connect"),
(JSONDecodeError("Test JSONDecodeError", "test", 1), "cannot_connect"),
(Exception("Test unknown exception"), "unknown"),
],
)
@@ -195,7 +195,7 @@ async def test_flow_source_import_error_and_recover(
"""Test exceptions and recovery."""
with patch(
"homeassistant.components.lupusec.config_flow.lupupy.Lupusec.__init__",
"homeassistant.components.lupusec.config_flow.lupupy.Lupusec",
side_effect=raise_error,
) as mock_initialize_lupusec:
result = await hass.config_entries.flow.async_init(
+10 -3
View File
@@ -11,7 +11,12 @@ from homeassistant.components.proximity.const import (
DOMAIN,
)
from homeassistant.components.script import scripts_with_entity
from homeassistant.const import CONF_ZONE, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.const import (
ATTR_FRIENDLY_NAME,
CONF_ZONE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
import homeassistant.helpers.issue_registry as ir
@@ -1205,7 +1210,7 @@ async def test_sensor_unique_ids(
) -> None:
"""Test that when tracked entity is renamed."""
t1 = entity_registry.async_get_or_create(
"device_tracker", "device_tracker", "test1"
"device_tracker", "device_tracker", "test1", original_name="Test tracker 1"
)
hass.states.async_set(t1.entity_id, "not_home")
@@ -1227,10 +1232,12 @@ async def test_sensor_unique_ids(
assert await hass.config_entries.async_setup(mock_config.entry_id)
await hass.async_block_till_done()
sensor_t1 = f"sensor.home_{t1.entity_id.split('.')[-1]}_distance"
sensor_t1 = "sensor.home_test_tracker_1_distance"
entity = entity_registry.async_get(sensor_t1)
assert entity
assert entity.unique_id == f"{mock_config.entry_id}_{t1.id}_dist_to_zone"
state = hass.states.get(sensor_t1)
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home Test tracker 1 Distance"
entity = entity_registry.async_get("sensor.home_test2_distance")
assert entity
+2 -2
View File
@@ -416,8 +416,8 @@ async def test_bandwidth_sensors(
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
assert hass.states.get("sensor.wireless_client_rx").state == "0"
assert hass.states.get("sensor.wireless_client_tx").state == "0"
assert hass.states.get("sensor.wireless_client_rx").state == STATE_UNAVAILABLE
assert hass.states.get("sensor.wireless_client_tx").state == STATE_UNAVAILABLE
# Disable option
+23 -6
View File
@@ -368,6 +368,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
"report_count",
"read_plug",
"unsupported_attrs",
"initial_sensor_state",
),
(
(
@@ -377,6 +378,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
1,
None,
None,
STATE_UNKNOWN,
),
(
measurement.TemperatureMeasurement.cluster_id,
@@ -385,6 +387,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
1,
None,
None,
STATE_UNKNOWN,
),
(
measurement.PressureMeasurement.cluster_id,
@@ -393,6 +396,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
1,
None,
None,
STATE_UNKNOWN,
),
(
measurement.IlluminanceMeasurement.cluster_id,
@@ -401,6 +405,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
1,
None,
None,
STATE_UNKNOWN,
),
(
smartenergy.Metering.cluster_id,
@@ -415,6 +420,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
"status": 0x00,
},
{"current_summ_delivered", "current_summ_received"},
STATE_UNKNOWN,
),
(
smartenergy.Metering.cluster_id,
@@ -431,6 +437,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
"unit_of_measure": 0x00,
},
{"instaneneous_demand", "current_summ_received"},
STATE_UNKNOWN,
),
(
smartenergy.Metering.cluster_id,
@@ -445,8 +452,10 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
"status": 0x00,
"summation_formatting": 0b1_0111_010,
"unit_of_measure": 0x00,
"current_summ_received": 0,
},
{"instaneneous_demand", "current_summ_delivered"},
"0.0",
),
(
homeautomation.ElectricalMeasurement.cluster_id,
@@ -455,6 +464,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
7,
{"ac_power_divisor": 1000, "ac_power_multiplier": 1},
{"apparent_power", "rms_current", "rms_voltage"},
STATE_UNKNOWN,
),
(
homeautomation.ElectricalMeasurement.cluster_id,
@@ -463,6 +473,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
7,
{"ac_power_divisor": 1000, "ac_power_multiplier": 1},
{"active_power", "rms_current", "rms_voltage"},
STATE_UNKNOWN,
),
(
homeautomation.ElectricalMeasurement.cluster_id,
@@ -471,6 +482,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
7,
{"ac_power_divisor": 1000, "ac_power_multiplier": 1},
{"active_power", "apparent_power", "rms_current", "rms_voltage"},
STATE_UNKNOWN,
),
(
homeautomation.ElectricalMeasurement.cluster_id,
@@ -479,6 +491,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
7,
{"ac_current_divisor": 1000, "ac_current_multiplier": 1},
{"active_power", "apparent_power", "rms_voltage"},
STATE_UNKNOWN,
),
(
homeautomation.ElectricalMeasurement.cluster_id,
@@ -487,6 +500,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
7,
{"ac_voltage_divisor": 10, "ac_voltage_multiplier": 1},
{"active_power", "apparent_power", "rms_current"},
STATE_UNKNOWN,
),
(
general.PowerConfiguration.cluster_id,
@@ -499,6 +513,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
"battery_quantity": 3,
},
None,
STATE_UNKNOWN,
),
(
general.PowerConfiguration.cluster_id,
@@ -511,6 +526,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
"battery_quantity": 3,
},
None,
STATE_UNKNOWN,
),
(
general.DeviceTemperature.cluster_id,
@@ -519,6 +535,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
1,
None,
None,
STATE_UNKNOWN,
),
(
hvac.Thermostat.cluster_id,
@@ -527,6 +544,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
10,
None,
None,
STATE_UNKNOWN,
),
(
hvac.Thermostat.cluster_id,
@@ -535,6 +553,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id):
10,
None,
None,
STATE_UNKNOWN,
),
),
)
@@ -548,6 +567,7 @@ async def test_sensor(
report_count,
read_plug,
unsupported_attrs,
initial_sensor_state,
) -> None:
"""Test ZHA sensor platform."""
@@ -582,8 +602,8 @@ async def test_sensor(
# allow traffic to flow through the gateway and devices
await async_enable_traffic(hass, [zha_device])
# test that the sensor now have a state of unknown
assert hass.states.get(entity_id).state == STATE_UNKNOWN
# test that the sensor now have their correct initial state (mostly unknown)
assert hass.states.get(entity_id).state == initial_sensor_state
# test sensor associated logic
await test_func(hass, cluster, entity_id)
@@ -826,7 +846,6 @@ async def test_electrical_measurement_init(
},
{
"summation_delivered",
"summation_received",
},
{
"instantaneous_demand",
@@ -834,12 +853,11 @@ async def test_electrical_measurement_init(
),
(
smartenergy.Metering.cluster_id,
{"instantaneous_demand", "current_summ_delivered", "current_summ_received"},
{"instantaneous_demand", "current_summ_delivered"},
{},
{
"instantaneous_demand",
"summation_delivered",
"summation_received",
},
),
(
@@ -848,7 +866,6 @@ async def test_electrical_measurement_init(
{
"instantaneous_demand",
"summation_delivered",
"summation_received",
},
{},
),
+1 -1
View File
@@ -205,7 +205,7 @@ def make_packet(zigpy_device, cluster, cmd_name: str, **kwargs):
command_id=cluster.commands_by_name[cmd_name].id,
schema=cluster.commands_by_name[cmd_name].schema,
disable_default_response=False,
direction=foundation.Direction.Server_to_Client,
direction=foundation.Direction.Client_to_Server,
args=(),
kwargs=kwargs,
)
-52
View File
@@ -243,11 +243,6 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_summation_delivered",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): {
DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_summation_received",
},
("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
DEV_SIG_CLUSTER_HANDLERS: ["basic"],
DEV_SIG_ENT_MAP_CLASS: "RSSISensor",
@@ -616,13 +611,6 @@ DEVICES = [
"sensor.climaxtechnology_psmp5_00_00_02_02tc_summation_delivered"
),
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): {
DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: (
"sensor.climaxtechnology_psmp5_00_00_02_02tc_summation_received"
),
},
("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
DEV_SIG_CLUSTER_HANDLERS: ["basic"],
DEV_SIG_ENT_MAP_CLASS: "RSSISensor",
@@ -1617,11 +1605,6 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_summation_delivered",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): {
DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_summation_received",
},
("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
DEV_SIG_CLUSTER_HANDLERS: ["basic"],
DEV_SIG_ENT_MAP_CLASS: "RSSISensor",
@@ -1682,11 +1665,6 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_summation_delivered",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): {
DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_summation_received",
},
("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
DEV_SIG_CLUSTER_HANDLERS: ["basic"],
DEV_SIG_ENT_MAP_CLASS: "RSSISensor",
@@ -1747,11 +1725,6 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_summation_delivered",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): {
DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_summation_received",
},
("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
DEV_SIG_CLUSTER_HANDLERS: ["basic"],
DEV_SIG_ENT_MAP_CLASS: "RSSISensor",
@@ -2370,11 +2343,6 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_summation_delivered",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): {
DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_summation_received",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794"): {
DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering",
@@ -4511,11 +4479,6 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_summation_delivered",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): {
DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_summation_received",
},
("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
DEV_SIG_CLUSTER_HANDLERS: ["basic"],
DEV_SIG_ENT_MAP_CLASS: "RSSISensor",
@@ -5362,11 +5325,6 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_summation_delivered",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): {
DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_summation_received",
},
("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
DEV_SIG_CLUSTER_HANDLERS: ["basic"],
DEV_SIG_ENT_MAP_CLASS: "RSSISensor",
@@ -5420,11 +5378,6 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_summation_delivered",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): {
DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_summation_received",
},
("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
DEV_SIG_CLUSTER_HANDLERS: ["basic"],
DEV_SIG_ENT_MAP_CLASS: "RSSISensor",
@@ -5478,11 +5431,6 @@ DEVICES = [
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_summation_delivered",
},
("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_received"): {
DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"],
DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation",
DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_summation_received",
},
("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
DEV_SIG_CLUSTER_HANDLERS: ["basic"],
DEV_SIG_ENT_MAP_CLASS: "RSSISensor",
@@ -396,19 +396,19 @@ async def test_abort_discovered_multiple(
HTTPStatus.UNAUTHORIZED,
{},
"oauth_unauthorized",
"Token request failed (unknown): unknown",
"Token request for oauth2_test failed (unknown): unknown",
),
(
HTTPStatus.NOT_FOUND,
{},
"oauth_failed",
"Token request failed (unknown): unknown",
"Token request for oauth2_test failed (unknown): unknown",
),
(
HTTPStatus.INTERNAL_SERVER_ERROR,
{},
"oauth_failed",
"Token request failed (unknown): unknown",
"Token request for oauth2_test failed (unknown): unknown",
),
(
HTTPStatus.BAD_REQUEST,
@@ -418,7 +418,7 @@ async def test_abort_discovered_multiple(
"error_uri": "See the full API docs at https://authorization-server.com/docs/access_token",
},
"oauth_failed",
"Token request failed (invalid_request): Request was missing the",
"Token request for oauth2_test failed (invalid_request): Request was missing the",
),
],
)
@@ -541,7 +541,7 @@ async def test_abort_if_oauth_token_closing_error(
with caplog.at_level(logging.DEBUG):
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert "Token request failed (unknown): unknown" in caplog.text
assert "Token request for oauth2_test failed (unknown): unknown" in caplog.text
assert result["type"] == data_entry_flow.FlowResultType.ABORT
assert result["reason"] == "oauth_unauthorized"
+82
View File
@@ -19,6 +19,7 @@ from homeassistant.core import (
)
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
from homeassistant.helpers import (
area_registry as ar,
device_registry as dr,
entity_platform,
entity_registry as er,
@@ -1628,6 +1629,87 @@ async def test_register_entity_service_response_data_multiple_matches_raises(
)
async def test_register_entity_service_limited_to_matching_platforms(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
area_registry: ar.AreaRegistry,
) -> None:
"""Test an entity services only targets entities for the platform and domain."""
mock_area = area_registry.async_get_or_create("mock_area")
entity1_entry = entity_registry.async_get_or_create(
"base_platform", "mock_platform", "1234", suggested_object_id="entity1"
)
entity_registry.async_update_entity(entity1_entry.entity_id, area_id=mock_area.id)
entity2_entry = entity_registry.async_get_or_create(
"base_platform", "mock_platform", "5678", suggested_object_id="entity2"
)
entity_registry.async_update_entity(entity2_entry.entity_id, area_id=mock_area.id)
entity3_entry = entity_registry.async_get_or_create(
"base_platform", "other_mock_platform", "7891", suggested_object_id="entity3"
)
entity_registry.async_update_entity(entity3_entry.entity_id, area_id=mock_area.id)
entity4_entry = entity_registry.async_get_or_create(
"base_platform", "other_mock_platform", "1433", suggested_object_id="entity4"
)
entity_registry.async_update_entity(entity4_entry.entity_id, area_id=mock_area.id)
async def generate_response(
target: MockEntity, call: ServiceCall
) -> ServiceResponse:
assert call.return_response
return {"response-key": f"response-value-{target.entity_id}"}
entity_platform = MockEntityPlatform(
hass, domain="base_platform", platform_name="mock_platform", platform=None
)
entity1 = MockEntity(
entity_id=entity1_entry.entity_id, unique_id=entity1_entry.unique_id
)
entity2 = MockEntity(
entity_id=entity2_entry.entity_id, unique_id=entity2_entry.unique_id
)
await entity_platform.async_add_entities([entity1, entity2])
other_entity_platform = MockEntityPlatform(
hass, domain="base_platform", platform_name="other_mock_platform", platform=None
)
entity3 = MockEntity(
entity_id=entity3_entry.entity_id, unique_id=entity3_entry.unique_id
)
entity4 = MockEntity(
entity_id=entity4_entry.entity_id, unique_id=entity4_entry.unique_id
)
await other_entity_platform.async_add_entities([entity3, entity4])
entity_platform.async_register_entity_service(
"hello",
{"some": str},
generate_response,
supports_response=SupportsResponse.ONLY,
)
response_data = await hass.services.async_call(
"mock_platform",
"hello",
service_data={"some": "data"},
target={"area_id": [mock_area.id]},
blocking=True,
return_response=True,
)
# We should not target entity3 and entity4 even though they are in the area
# because they are only part of the domain and not the platform
assert response_data == {
"base_platform.entity1": {
"response-key": "response-value-base_platform.entity1"
},
"base_platform.entity2": {
"response-key": "response-value-base_platform.entity2"
},
}
async def test_invalid_entity_id(hass: HomeAssistant) -> None:
"""Test specifying an invalid entity id."""
platform = MockEntityPlatform(hass)