forked from home-assistant/core
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9dbf84228e | |||
| 9e47d03086 | |||
| f63aaf8b5a | |||
| 8375fc235d | |||
| 3030870de0 | |||
| f61c70b686 | |||
| e720b398d2 | |||
| bd21490a57 | |||
| 75b308c1aa | |||
| 881707e1fe | |||
| 2ca3bbaea5 | |||
| ea4bdbb3a0 | |||
| 40cfc31dcb | |||
| 031aadff00 | |||
| 27691b7d48 | |||
| fe94107af7 | |||
| d784a76d32 | |||
| ebb1912617 | |||
| 8c605c29c3 | |||
| 74a75e709f | |||
| 2103875ff7 | |||
| 5c83b774bb | |||
| 2c870f9da9 | |||
| 40adb3809f | |||
| 8aa1242221 | |||
| 8569ddc5f9 | |||
| 7032415528 | |||
| d099fb2a26 | |||
| 35fad52913 | |||
| c170132827 | |||
| 439f82a4ec | |||
| 2481d14632 | |||
| 3cf826dc93 | |||
| e25ddf9650 | |||
| 8d79ac67f5 | |||
| 5025c15165 | |||
| ffd5e04a29 | |||
| 9fcdfd1b16 | |||
| c1e5b2e6cc | |||
| 31c0d21204 | |||
| 3ba63fc78f | |||
| 0395315267 | |||
| 6b354457c2 | |||
| df88335370 | |||
| 4c6c5ee63d | |||
| 65476914ed | |||
| d30a2e3611 | |||
| eb510e3630 | |||
| 532df5b5f1 | |||
| 1534f99c80 | |||
| a19aa9595a | |||
| e3191d098f | |||
| cc36071612 | |||
| 2d90ee8237 | |||
| 16266703df | |||
| dd2cc52119 | |||
| c48c8c25fa | |||
| 83a5659d57 | |||
| 3183cd346d | |||
| 5930c841d7 | |||
| f05ba22b5c |
@@ -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
|
||||
|
||||
@@ -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,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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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,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"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
{},
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user