Merge remote-tracking branch 'upstream/onvif_renew' into onvif_renew

This commit is contained in:
J. Nick Koston
2023-05-05 08:11:47 -05:00
30 changed files with 609 additions and 230 deletions

View File

@@ -3,7 +3,7 @@ from abc import ABC, abstractmethod
import asyncio import asyncio
import logging import logging
from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from .const import DOMAIN from .const import DOMAIN
@@ -19,7 +19,7 @@ class AbstractConfig(ABC):
_unsub_proactive_report: asyncio.Task[CALLBACK_TYPE] | None = None _unsub_proactive_report: asyncio.Task[CALLBACK_TYPE] | None = None
def __init__(self, hass): def __init__(self, hass: HomeAssistant) -> None:
"""Initialize abstract config.""" """Initialize abstract config."""
self.hass = hass self.hass = hass
self._store = None self._store = None

View File

@@ -199,14 +199,10 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
# Don't migrate if there's a YAML config # Don't migrate if there's a YAML config
return return
for state in self.hass.states.async_all(): for entity_id in {
async_expose_entity( *self.hass.states.async_entity_ids(),
self.hass, *self._prefs.alexa_entity_configs,
CLOUD_ALEXA, }:
state.entity_id,
self._should_expose_legacy(state.entity_id),
)
for entity_id in self._prefs.alexa_entity_configs:
async_expose_entity( async_expose_entity(
self.hass, self.hass,
CLOUD_ALEXA, CLOUD_ALEXA,
@@ -220,8 +216,18 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
async def on_hass_started(hass): async def on_hass_started(hass):
if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION: if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION:
if self._prefs.alexa_settings_version < 2: if self._prefs.alexa_settings_version < 2 or (
# Recover from a bug we had in 2023.5.0 where entities didn't get exposed
self._prefs.alexa_settings_version < 3
and not any(
settings.get("should_expose", False)
for settings in async_get_assistant_settings(
hass, CLOUD_ALEXA
).values()
)
):
self._migrate_alexa_entity_settings_v1() self._migrate_alexa_entity_settings_v1()
await self._prefs.async_update( await self._prefs.async_update(
alexa_settings_version=ALEXA_SETTINGS_VERSION alexa_settings_version=ALEXA_SETTINGS_VERSION
) )

View File

@@ -12,6 +12,7 @@ from homeassistant.components.google_assistant import DOMAIN as GOOGLE_DOMAIN
from homeassistant.components.google_assistant.helpers import AbstractConfig from homeassistant.components.google_assistant.helpers import AbstractConfig
from homeassistant.components.homeassistant.exposed_entities import ( from homeassistant.components.homeassistant.exposed_entities import (
async_expose_entity, async_expose_entity,
async_get_assistant_settings,
async_get_entity_settings, async_get_entity_settings,
async_listen_entity_updates, async_listen_entity_updates,
async_set_assistant_option, async_set_assistant_option,
@@ -175,23 +176,10 @@ class CloudGoogleConfig(AbstractConfig):
# Don't migrate if there's a YAML config # Don't migrate if there's a YAML config
return return
for state in self.hass.states.async_all(): for entity_id in {
entity_id = state.entity_id *self.hass.states.async_entity_ids(),
async_expose_entity( *self._prefs.google_entity_configs,
self.hass, }:
CLOUD_GOOGLE,
entity_id,
self._should_expose_legacy(entity_id),
)
if _2fa_disabled := (self._2fa_disabled_legacy(entity_id) is not None):
async_set_assistant_option(
self.hass,
CLOUD_GOOGLE,
entity_id,
PREF_DISABLE_2FA,
_2fa_disabled,
)
for entity_id in self._prefs.google_entity_configs:
async_expose_entity( async_expose_entity(
self.hass, self.hass,
CLOUD_GOOGLE, CLOUD_GOOGLE,
@@ -213,8 +201,18 @@ class CloudGoogleConfig(AbstractConfig):
async def on_hass_started(hass: HomeAssistant) -> None: async def on_hass_started(hass: HomeAssistant) -> None:
if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION: if self._prefs.google_settings_version != GOOGLE_SETTINGS_VERSION:
if self._prefs.google_settings_version < 2: if self._prefs.google_settings_version < 2 or (
# Recover from a bug we had in 2023.5.0 where entities didn't get exposed
self._prefs.google_settings_version < 3
and not any(
settings.get("should_expose", False)
for settings in async_get_assistant_settings(
hass, CLOUD_GOOGLE
).values()
)
):
self._migrate_google_entity_settings_v1() self._migrate_google_entity_settings_v1()
await self._prefs.async_update( await self._prefs.async_update(
google_settings_version=GOOGLE_SETTINGS_VERSION google_settings_version=GOOGLE_SETTINGS_VERSION
) )

View File

@@ -41,8 +41,8 @@ STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1 STORAGE_VERSION = 1
STORAGE_VERSION_MINOR = 2 STORAGE_VERSION_MINOR = 2
ALEXA_SETTINGS_VERSION = 2 ALEXA_SETTINGS_VERSION = 3
GOOGLE_SETTINGS_VERSION = 2 GOOGLE_SETTINGS_VERSION = 3
class CloudPreferencesStore(Store): class CloudPreferencesStore(Store):

View File

@@ -15,5 +15,5 @@
"documentation": "https://www.home-assistant.io/integrations/elkm1", "documentation": "https://www.home-assistant.io/integrations/elkm1",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["elkm1_lib"], "loggers": ["elkm1_lib"],
"requirements": ["elkm1-lib==2.2.1"] "requirements": ["elkm1-lib==2.2.2"]
} }

View File

@@ -25,6 +25,7 @@ from aioesphomeapi import (
NumberInfo, NumberInfo,
SelectInfo, SelectInfo,
SensorInfo, SensorInfo,
SensorState,
SwitchInfo, SwitchInfo,
TextSensorInfo, TextSensorInfo,
UserService, UserService,
@@ -240,9 +241,18 @@ class RuntimeEntryData:
current_state_by_type = self.state[state_type] current_state_by_type = self.state[state_type]
current_state = current_state_by_type.get(key, _SENTINEL) current_state = current_state_by_type.get(key, _SENTINEL)
subscription_key = (state_type, key) subscription_key = (state_type, key)
if current_state == state and subscription_key not in stale_state: if (
current_state == state
and subscription_key not in stale_state
and not (
type(state) is SensorState # pylint: disable=unidiomatic-typecheck
and (platform_info := self.info.get(Platform.SENSOR))
and (entity_info := platform_info.get(state.key))
and (cast(SensorInfo, entity_info)).force_update
)
):
_LOGGER.debug( _LOGGER.debug(
"%s: ignoring duplicate update with and key %s: %s", "%s: ignoring duplicate update with key %s: %s",
self.name, self.name,
key, key,
state, state,

View File

@@ -15,7 +15,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aioesphomeapi", "noiseprotocol"], "loggers": ["aioesphomeapi", "noiseprotocol"],
"requirements": [ "requirements": [
"aioesphomeapi==13.7.2", "aioesphomeapi==13.7.3",
"bluetooth-data-tools==0.4.0", "bluetooth-data-tools==0.4.0",
"esphome-dashboard-api==1.2.3" "esphome-dashboard-api==1.2.3"
], ],

View File

@@ -1,6 +1,6 @@
"""Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org.""" """Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org."""
import asyncio import asyncio
from datetime import timedelta from datetime import datetime, timedelta
import logging import logging
import aiohttp import aiohttp
@@ -53,11 +53,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if result is False: if result is False:
return False return False
async def update_domain_callback(now): async def update_domain_callback(now: datetime) -> None:
"""Update the FreeDNS entry.""" """Update the FreeDNS entry."""
await _update_freedns(hass, session, url, auth_token) await _update_freedns(hass, session, url, auth_token)
async_track_time_interval(hass, update_domain_callback, update_interval) async_track_time_interval(
hass, update_domain_callback, update_interval, cancel_on_shutdown=True
)
return True return True

View File

@@ -590,7 +590,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
await async_setup_addon_panel(hass, hassio) await async_setup_addon_panel(hass, hassio)
# Setup hardware integration for the detected board type # Setup hardware integration for the detected board type
async def _async_setup_hardware_integration(hass): async def _async_setup_hardware_integration(_: datetime) -> None:
"""Set up hardaware integration for the detected board type.""" """Set up hardaware integration for the detected board type."""
if (os_info := get_os_info(hass)) is None: if (os_info := get_os_info(hass)) is None:
# os info not yet fetched from supervisor, retry later # os info not yet fetched from supervisor, retry later
@@ -610,7 +610,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
) )
) )
await _async_setup_hardware_integration(hass) await _async_setup_hardware_integration(datetime.now())
hass.async_create_task( hass.async_create_task(
hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"}) hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"})

View File

@@ -31,7 +31,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
) )
from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
from homeassistant.helpers import discovery, event from homeassistant.helpers import discovery, event
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
@@ -208,16 +208,18 @@ def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901
def _adapter_watchdog(now=None): def _adapter_watchdog(now=None):
_LOGGER.debug("Reached _adapter_watchdog") _LOGGER.debug("Reached _adapter_watchdog")
event.call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog) event.call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog_job)
if not adapter.initialized: if not adapter.initialized:
_LOGGER.info("Adapter not initialized; Trying to restart") _LOGGER.info("Adapter not initialized; Trying to restart")
hass.bus.fire(EVENT_HDMI_CEC_UNAVAILABLE) hass.bus.fire(EVENT_HDMI_CEC_UNAVAILABLE)
adapter.init() adapter.init()
_adapter_watchdog_job = HassJob(_adapter_watchdog, cancel_on_shutdown=True)
@callback @callback
def _async_initialized_callback(*_: Any): def _async_initialized_callback(*_: Any):
"""Add watchdog on initialization.""" """Add watchdog on initialization."""
return event.async_call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog) return event.async_call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog_job)
hdmi_network.set_initialized_callback(_async_initialized_callback) hdmi_network.set_initialized_callback(_async_initialized_callback)

View File

@@ -67,7 +67,7 @@ async def async_setup_platform(
if isinstance(rest.last_exception, ssl.SSLError): if isinstance(rest.last_exception, ssl.SSLError):
_LOGGER.error( _LOGGER.error(
"Error connecting %s failed with %s", "Error connecting %s failed with %s",
conf[CONF_RESOURCE], rest.url,
rest.last_exception, rest.last_exception,
) )
return return

View File

@@ -50,6 +50,11 @@ class RestData:
self.last_exception: Exception | None = None self.last_exception: Exception | None = None
self.headers: httpx.Headers | None = None self.headers: httpx.Headers | None = None
@property
def url(self) -> str:
"""Get url."""
return self._resource
def set_url(self, url: str) -> None: def set_url(self, url: str) -> None:
"""Set url.""" """Set url."""
self._resource = url self._resource = url

View File

@@ -71,7 +71,7 @@ async def async_setup_platform(
if isinstance(rest.last_exception, ssl.SSLError): if isinstance(rest.last_exception, ssl.SSLError):
_LOGGER.error( _LOGGER.error(
"Error connecting %s failed with %s", "Error connecting %s failed with %s",
conf[CONF_RESOURCE], rest.url,
rest.last_exception, rest.last_exception,
) )
return return

View File

@@ -6,8 +6,8 @@ from http import HTTPStatus
import logging import logging
from typing import Any from typing import Any
import aiohttp
import async_timeout import async_timeout
import httpx
import voluptuous as vol import voluptuous as vol
from homeassistant.components.switch import ( from homeassistant.components.switch import (
@@ -30,8 +30,8 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.template_entity import ( from homeassistant.helpers.template_entity import (
TEMPLATE_ENTITY_BASE_SCHEMA, TEMPLATE_ENTITY_BASE_SCHEMA,
TemplateEntity, TemplateEntity,
@@ -89,8 +89,8 @@ async def async_setup_platform(
switch = RestSwitch(hass, config, unique_id) switch = RestSwitch(hass, config, unique_id)
req = await switch.get_device_state(hass) req = await switch.get_device_state(hass)
if req.status >= HTTPStatus.BAD_REQUEST: if req.status_code >= HTTPStatus.BAD_REQUEST:
_LOGGER.error("Got non-ok response from resource: %s", req.status) _LOGGER.error("Got non-ok response from resource: %s", req.status_code)
else: else:
async_add_entities([switch]) async_add_entities([switch])
except (TypeError, ValueError): except (TypeError, ValueError):
@@ -98,7 +98,7 @@ async def async_setup_platform(
"Missing resource or schema in configuration. " "Missing resource or schema in configuration. "
"Add http:// or https:// to your URL" "Add http:// or https:// to your URL"
) )
except (asyncio.TimeoutError, aiohttp.ClientError) as exc: except (asyncio.TimeoutError, httpx.RequestError) as exc:
raise PlatformNotReady(f"No route to resource/endpoint: {resource}") from exc raise PlatformNotReady(f"No route to resource/endpoint: {resource}") from exc
@@ -120,11 +120,11 @@ class RestSwitch(TemplateEntity, SwitchEntity):
unique_id=unique_id, unique_id=unique_id,
) )
auth: aiohttp.BasicAuth | None = None auth: httpx.BasicAuth | None = None
username: str | None = None username: str | None = None
if username := config.get(CONF_USERNAME): if username := config.get(CONF_USERNAME):
password: str = config[CONF_PASSWORD] password: str = config[CONF_PASSWORD]
auth = aiohttp.BasicAuth(username, password=password) auth = httpx.BasicAuth(username, password=password)
self._resource: str = config[CONF_RESOURCE] self._resource: str = config[CONF_RESOURCE]
self._state_resource: str = config.get(CONF_STATE_RESOURCE) or self._resource self._state_resource: str = config.get(CONF_STATE_RESOURCE) or self._resource
@@ -155,13 +155,13 @@ class RestSwitch(TemplateEntity, SwitchEntity):
try: try:
req = await self.set_device_state(body_on_t) req = await self.set_device_state(body_on_t)
if req.status == HTTPStatus.OK: if req.status_code == HTTPStatus.OK:
self._attr_is_on = True self._attr_is_on = True
else: else:
_LOGGER.error( _LOGGER.error(
"Can't turn on %s. Is resource/endpoint offline?", self._resource "Can't turn on %s. Is resource/endpoint offline?", self._resource
) )
except (asyncio.TimeoutError, aiohttp.ClientError): except (asyncio.TimeoutError, httpx.RequestError):
_LOGGER.error("Error while switching on %s", self._resource) _LOGGER.error("Error while switching on %s", self._resource)
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
@@ -170,24 +170,24 @@ class RestSwitch(TemplateEntity, SwitchEntity):
try: try:
req = await self.set_device_state(body_off_t) req = await self.set_device_state(body_off_t)
if req.status == HTTPStatus.OK: if req.status_code == HTTPStatus.OK:
self._attr_is_on = False self._attr_is_on = False
else: else:
_LOGGER.error( _LOGGER.error(
"Can't turn off %s. Is resource/endpoint offline?", self._resource "Can't turn off %s. Is resource/endpoint offline?", self._resource
) )
except (asyncio.TimeoutError, aiohttp.ClientError): except (asyncio.TimeoutError, httpx.RequestError):
_LOGGER.error("Error while switching off %s", self._resource) _LOGGER.error("Error while switching off %s", self._resource)
async def set_device_state(self, body: Any) -> aiohttp.ClientResponse: async def set_device_state(self, body: Any) -> httpx.Response:
"""Send a state update to the device.""" """Send a state update to the device."""
websession = async_get_clientsession(self.hass, self._verify_ssl) websession = get_async_client(self.hass, self._verify_ssl)
rendered_headers = template.render_complex(self._headers, parse_result=False) rendered_headers = template.render_complex(self._headers, parse_result=False)
rendered_params = template.render_complex(self._params) rendered_params = template.render_complex(self._params)
async with async_timeout.timeout(self._timeout): async with async_timeout.timeout(self._timeout):
req: aiohttp.ClientResponse = await getattr(websession, self._method)( req: httpx.Response = await getattr(websession, self._method)(
self._resource, self._resource,
auth=self._auth, auth=self._auth,
data=bytes(body, "utf-8"), data=bytes(body, "utf-8"),
@@ -202,12 +202,12 @@ class RestSwitch(TemplateEntity, SwitchEntity):
await self.get_device_state(self.hass) await self.get_device_state(self.hass)
except asyncio.TimeoutError: except asyncio.TimeoutError:
_LOGGER.exception("Timed out while fetching data") _LOGGER.exception("Timed out while fetching data")
except aiohttp.ClientError as err: except httpx.RequestError as err:
_LOGGER.exception("Error while fetching data: %s", err) _LOGGER.exception("Error while fetching data: %s", err)
async def get_device_state(self, hass: HomeAssistant) -> aiohttp.ClientResponse: async def get_device_state(self, hass: HomeAssistant) -> httpx.Response:
"""Get the latest data from REST API and update the state.""" """Get the latest data from REST API and update the state."""
websession = async_get_clientsession(hass, self._verify_ssl) websession = get_async_client(hass, self._verify_ssl)
rendered_headers = template.render_complex(self._headers, parse_result=False) rendered_headers = template.render_complex(self._headers, parse_result=False)
rendered_params = template.render_complex(self._params) rendered_params = template.render_complex(self._params)
@@ -219,7 +219,7 @@ class RestSwitch(TemplateEntity, SwitchEntity):
headers=rendered_headers, headers=rendered_headers,
params=rendered_params, params=rendered_params,
) )
text = await req.text() text = req.text
if self._is_on_template is not None: if self._is_on_template is not None:
text = self._is_on_template.async_render_with_possible_json_value( text = self._is_on_template.async_render_with_possible_json_value(

View File

@@ -123,7 +123,7 @@ class SIAAlarmControlPanel(SIABaseEntity, AlarmControlPanelEntity):
""" """
new_state = None new_state = None
if sia_event.code: if sia_event.code:
new_state = self.entity_description.code_consequences[sia_event.code] new_state = self.entity_description.code_consequences.get(sia_event.code)
if new_state is None: if new_state is None:
return False return False
_LOGGER.debug("New state will be %s", new_state) _LOGGER.debug("New state will be %s", new_state)

View File

@@ -132,7 +132,7 @@ class SIABinarySensor(SIABaseEntity, BinarySensorEntity):
""" """
new_state = None new_state = None
if sia_event.code: if sia_event.code:
new_state = self.entity_description.code_consequences[sia_event.code] new_state = self.entity_description.code_consequences.get(sia_event.code)
if new_state is None: if new_state is None:
return False return False
_LOGGER.debug("New state will be %s", new_state) _LOGGER.debug("New state will be %s", new_state)

View File

@@ -8,5 +8,5 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["hatasmota"], "loggers": ["hatasmota"],
"mqtt": ["tasmota/discovery/#"], "mqtt": ["tasmota/discovery/#"],
"requirements": ["hatasmota==0.6.4"] "requirements": ["hatasmota==0.6.5"]
} }

View File

@@ -12,6 +12,12 @@ from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
TextSelector,
)
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .const import CONF_FROM, CONF_TIME, CONF_TO, DOMAIN from .const import CONF_FROM, CONF_TIME, CONF_TO, DOMAIN
@@ -23,12 +29,17 @@ ERROR_MULTIPLE_STATION = "Found multiple stations with the specified name"
DATA_SCHEMA = vol.Schema( DATA_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_API_KEY): TextSelector(),
vol.Required(CONF_FROM): cv.string, vol.Required(CONF_FROM): TextSelector(),
vol.Required(CONF_TO): cv.string, vol.Required(CONF_TO): TextSelector(),
vol.Optional(CONF_TIME): cv.string, vol.Optional(CONF_TIME): TextSelector(),
vol.Required(CONF_WEEKDAY, default=WEEKDAYS): cv.multi_select( vol.Required(CONF_WEEKDAY, default=WEEKDAYS): SelectSelector(
{day: day for day in WEEKDAYS} SelectSelectorConfig(
options=WEEKDAYS,
multiple=True,
mode=SelectSelectorMode.DROPDOWN,
translation_key=CONF_WEEKDAY,
)
), ),
} }
) )

View File

@@ -28,5 +28,18 @@
} }
} }
} }
},
"selector": {
"weekday": {
"options": {
"mon": "Monday",
"tue": "Tuesday",
"wed": "Wednesday",
"thu": "Thursday",
"fri": "Friday",
"sat": "Saturday",
"sun": "Sunday"
}
}
} }
} }

View File

@@ -7,7 +7,11 @@ import logging
from typing import Any from typing import Any
import transmission_rpc import transmission_rpc
from transmission_rpc.error import TransmissionError from transmission_rpc.error import (
TransmissionAuthError,
TransmissionConnectError,
TransmissionError,
)
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.config_entries import ConfigEntry, ConfigEntryState
@@ -137,14 +141,13 @@ async def get_api(hass, entry):
_LOGGER.debug("Successfully connected to %s", host) _LOGGER.debug("Successfully connected to %s", host)
return api return api
except TransmissionAuthError as error:
_LOGGER.error("Credentials for Transmission client are not valid")
raise AuthenticationError from error
except TransmissionConnectError as error:
_LOGGER.error("Connecting to the Transmission client %s failed", host)
raise CannotConnect from error
except TransmissionError as error: except TransmissionError as error:
if "401: Unauthorized" in str(error):
_LOGGER.error("Credentials for Transmission client are not valid")
raise AuthenticationError from error
if "111: Connection refused" in str(error):
_LOGGER.error("Connecting to the Transmission client %s failed", host)
raise CannotConnect from error
_LOGGER.error(error) _LOGGER.error(error)
raise UnknownError from error raise UnknownError from error

View File

@@ -137,7 +137,19 @@ class Endpoint:
): ):
cluster_handler_class = MultistateInput cluster_handler_class = MultistateInput
# end of ugly hack # end of ugly hack
cluster_handler = cluster_handler_class(cluster, self)
try:
cluster_handler = cluster_handler_class(cluster, self)
except KeyError as err:
_LOGGER.warning(
"Cluster handler %s for cluster %s on endpoint %s is invalid: %s",
cluster_handler_class,
cluster,
self,
err,
)
continue
if cluster_handler.name == const.CLUSTER_HANDLER_POWER_CONFIGURATION: if cluster_handler.name == const.CLUSTER_HANDLER_POWER_CONFIGURATION:
self._device.power_configuration_ch = cluster_handler self._device.power_configuration_ch = cluster_handler
elif cluster_handler.name == const.CLUSTER_HANDLER_IDENTIFY: elif cluster_handler.name == const.CLUSTER_HANDLER_IDENTIFY:

View File

@@ -156,7 +156,7 @@ aioecowitt==2023.01.0
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==13.7.2 aioesphomeapi==13.7.3
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@@ -644,7 +644,7 @@ elgato==4.0.1
eliqonline==1.2.2 eliqonline==1.2.2
# homeassistant.components.elkm1 # homeassistant.components.elkm1
elkm1-lib==2.2.1 elkm1-lib==2.2.2
# homeassistant.components.elmax # homeassistant.components.elmax
elmax_api==0.0.4 elmax_api==0.0.4
@@ -881,7 +881,7 @@ hass_splunk==0.1.1
hassil==1.0.6 hassil==1.0.6
# homeassistant.components.tasmota # homeassistant.components.tasmota
hatasmota==0.6.4 hatasmota==0.6.5
# homeassistant.components.jewish_calendar # homeassistant.components.jewish_calendar
hdate==0.10.4 hdate==0.10.4

View File

@@ -146,7 +146,7 @@ aioecowitt==2023.01.0
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==13.7.2 aioesphomeapi==13.7.3
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@@ -506,7 +506,7 @@ easyenergy==0.3.0
elgato==4.0.1 elgato==4.0.1
# homeassistant.components.elkm1 # homeassistant.components.elkm1
elkm1-lib==2.2.1 elkm1-lib==2.2.2
# homeassistant.components.elmax # homeassistant.components.elmax
elmax_api==0.0.4 elmax_api==0.0.4
@@ -679,7 +679,7 @@ hass-nabucasa==0.66.2
hassil==1.0.6 hassil==1.0.6
# homeassistant.components.tasmota # homeassistant.components.tasmota
hatasmota==0.6.4 hatasmota==0.6.5
# homeassistant.components.jewish_calendar # homeassistant.components.jewish_calendar
hdate==0.10.4 hdate==0.10.4

View File

@@ -542,11 +542,13 @@ async def test_alexa_handle_logout(
assert len(mock_enable.return_value.mock_calls) == 1 assert len(mock_enable.return_value.mock_calls) == 1
@pytest.mark.parametrize("alexa_settings_version", [1, 2])
async def test_alexa_config_migrate_expose_entity_prefs( async def test_alexa_config_migrate_expose_entity_prefs(
hass: HomeAssistant, hass: HomeAssistant,
cloud_prefs: CloudPreferences, cloud_prefs: CloudPreferences,
cloud_stub, cloud_stub,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
alexa_settings_version: int,
) -> None: ) -> None:
"""Test migrating Alexa entity config.""" """Test migrating Alexa entity config."""
hass.state = CoreState.starting hass.state = CoreState.starting
@@ -593,7 +595,7 @@ async def test_alexa_config_migrate_expose_entity_prefs(
await cloud_prefs.async_update( await cloud_prefs.async_update(
alexa_enabled=True, alexa_enabled=True,
alexa_report_state=False, alexa_report_state=False,
alexa_settings_version=1, alexa_settings_version=alexa_settings_version,
) )
expose_entity(hass, entity_migrated.entity_id, False) expose_entity(hass, entity_migrated.entity_id, False)
@@ -641,6 +643,100 @@ async def test_alexa_config_migrate_expose_entity_prefs(
} }
async def test_alexa_config_migrate_expose_entity_prefs_v2_no_exposed(
hass: HomeAssistant,
cloud_prefs: CloudPreferences,
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Alexa entity config from v2 to v3 when no entity is exposed."""
hass.state = CoreState.starting
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set("light.state_only", "on")
entity_migrated = entity_registry.async_get_or_create(
"light",
"test",
"light_migrated",
suggested_object_id="migrated",
)
await cloud_prefs.async_update(
alexa_enabled=True,
alexa_report_state=False,
alexa_settings_version=2,
)
expose_entity(hass, "light.state_only", False)
expose_entity(hass, entity_migrated.entity_id, False)
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS]["light.state_only"] = {
PREF_SHOULD_EXPOSE: True
}
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS][entity_migrated.entity_id] = {
PREF_SHOULD_EXPOSE: True
}
conf = alexa_config.CloudAlexaConfig(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False)
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert async_get_entity_settings(hass, "light.state_only") == {
"cloud.alexa": {"should_expose": True}
}
assert async_get_entity_settings(hass, entity_migrated.entity_id) == {
"cloud.alexa": {"should_expose": True}
}
async def test_alexa_config_migrate_expose_entity_prefs_v2_exposed(
hass: HomeAssistant,
cloud_prefs: CloudPreferences,
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Alexa entity config from v2 to v3 when an entity is exposed."""
hass.state = CoreState.starting
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set("light.state_only", "on")
entity_migrated = entity_registry.async_get_or_create(
"light",
"test",
"light_migrated",
suggested_object_id="migrated",
)
await cloud_prefs.async_update(
alexa_enabled=True,
alexa_report_state=False,
alexa_settings_version=2,
)
expose_entity(hass, "light.state_only", False)
expose_entity(hass, entity_migrated.entity_id, True)
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS]["light.state_only"] = {
PREF_SHOULD_EXPOSE: True
}
cloud_prefs._prefs[PREF_ALEXA_ENTITY_CONFIGS][entity_migrated.entity_id] = {
PREF_SHOULD_EXPOSE: True
}
conf = alexa_config.CloudAlexaConfig(
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False)
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert async_get_entity_settings(hass, "light.state_only") == {
"cloud.alexa": {"should_expose": False}
}
assert async_get_entity_settings(hass, entity_migrated.entity_id) == {
"cloud.alexa": {"should_expose": True}
}
async def test_alexa_config_migrate_expose_entity_prefs_default_none( async def test_alexa_config_migrate_expose_entity_prefs_default_none(
hass: HomeAssistant, hass: HomeAssistant,
cloud_prefs: CloudPreferences, cloud_prefs: CloudPreferences,

View File

@@ -483,10 +483,12 @@ async def test_google_handle_logout(
assert len(mock_enable.return_value.mock_calls) == 1 assert len(mock_enable.return_value.mock_calls) == 1
@pytest.mark.parametrize("google_settings_version", [1, 2])
async def test_google_config_migrate_expose_entity_prefs( async def test_google_config_migrate_expose_entity_prefs(
hass: HomeAssistant, hass: HomeAssistant,
cloud_prefs: CloudPreferences, cloud_prefs: CloudPreferences,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
google_settings_version: int,
) -> None: ) -> None:
"""Test migrating Google entity config.""" """Test migrating Google entity config."""
hass.state = CoreState.starting hass.state = CoreState.starting
@@ -540,7 +542,7 @@ async def test_google_config_migrate_expose_entity_prefs(
await cloud_prefs.async_update( await cloud_prefs.async_update(
google_enabled=True, google_enabled=True,
google_report_state=False, google_report_state=False,
google_settings_version=1, google_settings_version=google_settings_version,
) )
expose_entity(hass, entity_migrated.entity_id, False) expose_entity(hass, entity_migrated.entity_id, False)
@@ -596,6 +598,100 @@ async def test_google_config_migrate_expose_entity_prefs(
} }
async def test_google_config_migrate_expose_entity_prefs_v2_no_exposed(
hass: HomeAssistant,
cloud_prefs: CloudPreferences,
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Google entity config from v2 to v3 when no entity is exposed."""
hass.state = CoreState.starting
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set("light.state_only", "on")
entity_migrated = entity_registry.async_get_or_create(
"light",
"test",
"light_migrated",
suggested_object_id="migrated",
)
await cloud_prefs.async_update(
google_enabled=True,
google_report_state=False,
google_settings_version=2,
)
expose_entity(hass, "light.state_only", False)
expose_entity(hass, entity_migrated.entity_id, False)
cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS]["light.state_only"] = {
PREF_SHOULD_EXPOSE: True
}
cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS][entity_migrated.entity_id] = {
PREF_SHOULD_EXPOSE: True
}
conf = CloudGoogleConfig(
hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False)
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert async_get_entity_settings(hass, "light.state_only") == {
"cloud.google_assistant": {"should_expose": True}
}
assert async_get_entity_settings(hass, entity_migrated.entity_id) == {
"cloud.google_assistant": {"should_expose": True}
}
async def test_google_config_migrate_expose_entity_prefs_v2_exposed(
hass: HomeAssistant,
cloud_prefs: CloudPreferences,
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating Google entity config from v2 to v3 when an entity is exposed."""
hass.state = CoreState.starting
assert await async_setup_component(hass, "homeassistant", {})
hass.states.async_set("light.state_only", "on")
entity_migrated = entity_registry.async_get_or_create(
"light",
"test",
"light_migrated",
suggested_object_id="migrated",
)
await cloud_prefs.async_update(
google_enabled=True,
google_report_state=False,
google_settings_version=2,
)
expose_entity(hass, "light.state_only", False)
expose_entity(hass, entity_migrated.entity_id, True)
cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS]["light.state_only"] = {
PREF_SHOULD_EXPOSE: True
}
cloud_prefs._prefs[PREF_GOOGLE_ENTITY_CONFIGS][entity_migrated.entity_id] = {
PREF_SHOULD_EXPOSE: True
}
conf = CloudGoogleConfig(
hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(is_logged_in=False)
)
await conf.async_initialize()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert async_get_entity_settings(hass, "light.state_only") == {
"cloud.google_assistant": {"should_expose": False}
}
assert async_get_entity_settings(hass, entity_migrated.entity_id) == {
"cloud.google_assistant": {"should_expose": True}
}
async def test_google_config_migrate_expose_entity_prefs_default_none( async def test_google_config_migrate_expose_entity_prefs_default_none(
hass: HomeAssistant, hass: HomeAssistant,
cloud_prefs: CloudPreferences, cloud_prefs: CloudPreferences,

View File

@@ -2,8 +2,9 @@
import asyncio import asyncio
from http import HTTPStatus from http import HTTPStatus
import aiohttp import httpx
import pytest import pytest
import respx
from homeassistant.components.rest import DOMAIN from homeassistant.components.rest import DOMAIN
from homeassistant.components.rest.switch import ( from homeassistant.components.rest.switch import (
@@ -45,7 +46,6 @@ from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from tests.common import assert_setup_component, async_fire_time_changed from tests.common import assert_setup_component, async_fire_time_changed
from tests.test_util.aiohttp import AiohttpClientMocker
NAME = "foo" NAME = "foo"
DEVICE_CLASS = SwitchDeviceClass.SWITCH DEVICE_CLASS = SwitchDeviceClass.SWITCH
@@ -75,13 +75,13 @@ async def test_setup_missing_schema(
assert "Invalid config for [switch.rest]: invalid url" in caplog.text assert "Invalid config for [switch.rest]: invalid url" in caplog.text
@respx.mock
async def test_setup_failed_connect( async def test_setup_failed_connect(
hass: HomeAssistant, hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test setup when connection error occurs.""" """Test setup when connection error occurs."""
aioclient_mock.get(RESOURCE, exc=aiohttp.ClientError) respx.get(RESOURCE).mock(side_effect=asyncio.TimeoutError())
config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}} config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}}
assert await async_setup_component(hass, SWITCH_DOMAIN, config) assert await async_setup_component(hass, SWITCH_DOMAIN, config)
await hass.async_block_till_done() await hass.async_block_till_done()
@@ -89,13 +89,13 @@ async def test_setup_failed_connect(
assert "No route to resource/endpoint" in caplog.text assert "No route to resource/endpoint" in caplog.text
@respx.mock
async def test_setup_timeout( async def test_setup_timeout(
hass: HomeAssistant, hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test setup when connection timeout occurs.""" """Test setup when connection timeout occurs."""
aioclient_mock.get(RESOURCE, exc=asyncio.TimeoutError()) respx.get(RESOURCE).mock(side_effect=asyncio.TimeoutError())
config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}} config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}}
assert await async_setup_component(hass, SWITCH_DOMAIN, config) assert await async_setup_component(hass, SWITCH_DOMAIN, config)
await hass.async_block_till_done() await hass.async_block_till_done()
@@ -103,23 +103,21 @@ async def test_setup_timeout(
assert "No route to resource/endpoint" in caplog.text assert "No route to resource/endpoint" in caplog.text
async def test_setup_minimum( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_setup_minimum(hass: HomeAssistant) -> None:
) -> None:
"""Test setup with minimum configuration.""" """Test setup with minimum configuration."""
aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) route = respx.get(RESOURCE) % HTTPStatus.OK
config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}} config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}}
with assert_setup_component(1, SWITCH_DOMAIN): with assert_setup_component(1, SWITCH_DOMAIN):
assert await async_setup_component(hass, SWITCH_DOMAIN, config) assert await async_setup_component(hass, SWITCH_DOMAIN, config)
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.call_count == 1 assert route.call_count == 1
async def test_setup_query_params( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_setup_query_params(hass: HomeAssistant) -> None:
) -> None:
"""Test setup with query params.""" """Test setup with query params."""
aioclient_mock.get("http://localhost/?search=something", status=HTTPStatus.OK) route = respx.get("http://localhost/?search=something") % HTTPStatus.OK
config = { config = {
SWITCH_DOMAIN: { SWITCH_DOMAIN: {
CONF_PLATFORM: DOMAIN, CONF_PLATFORM: DOMAIN,
@@ -131,12 +129,13 @@ async def test_setup_query_params(
assert await async_setup_component(hass, SWITCH_DOMAIN, config) assert await async_setup_component(hass, SWITCH_DOMAIN, config)
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.call_count == 1 assert route.call_count == 1
async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: @respx.mock
async def test_setup(hass: HomeAssistant) -> None:
"""Test setup with valid configuration.""" """Test setup with valid configuration."""
aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) route = respx.get(RESOURCE) % HTTPStatus.OK
config = { config = {
SWITCH_DOMAIN: { SWITCH_DOMAIN: {
CONF_PLATFORM: DOMAIN, CONF_PLATFORM: DOMAIN,
@@ -149,16 +148,15 @@ async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -
} }
assert await async_setup_component(hass, SWITCH_DOMAIN, config) assert await async_setup_component(hass, SWITCH_DOMAIN, config)
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.call_count == 1 assert route.call_count == 1
assert_setup_component(1, SWITCH_DOMAIN) assert_setup_component(1, SWITCH_DOMAIN)
async def test_setup_with_state_resource( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_setup_with_state_resource(hass: HomeAssistant) -> None:
) -> None:
"""Test setup with valid configuration.""" """Test setup with valid configuration."""
aioclient_mock.get(RESOURCE, status=HTTPStatus.NOT_FOUND) respx.get(RESOURCE) % HTTPStatus.NOT_FOUND
aioclient_mock.get("http://localhost/state", status=HTTPStatus.OK) route = respx.get("http://localhost/state") % HTTPStatus.OK
config = { config = {
SWITCH_DOMAIN: { SWITCH_DOMAIN: {
CONF_PLATFORM: DOMAIN, CONF_PLATFORM: DOMAIN,
@@ -172,15 +170,14 @@ async def test_setup_with_state_resource(
} }
assert await async_setup_component(hass, SWITCH_DOMAIN, config) assert await async_setup_component(hass, SWITCH_DOMAIN, config)
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.call_count == 1 assert route.call_count == 1
assert_setup_component(1, SWITCH_DOMAIN) assert_setup_component(1, SWITCH_DOMAIN)
async def test_setup_with_templated_headers_params( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_setup_with_templated_headers_params(hass: HomeAssistant) -> None:
) -> None:
"""Test setup with valid configuration.""" """Test setup with valid configuration."""
aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) route = respx.get(RESOURCE) % HTTPStatus.OK
config = { config = {
SWITCH_DOMAIN: { SWITCH_DOMAIN: {
CONF_PLATFORM: DOMAIN, CONF_PLATFORM: DOMAIN,
@@ -198,21 +195,21 @@ async def test_setup_with_templated_headers_params(
} }
assert await async_setup_component(hass, SWITCH_DOMAIN, config) assert await async_setup_component(hass, SWITCH_DOMAIN, config)
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.call_count == 1 assert route.call_count == 1
assert aioclient_mock.mock_calls[-1][3].get("Accept") == CONTENT_TYPE_JSON last_call = route.calls[-1]
assert aioclient_mock.mock_calls[-1][3].get("User-Agent") == "Mozilla/5.0" last_request: httpx.Request = last_call.request
assert aioclient_mock.mock_calls[-1][1].query["start"] == "0" assert last_request.headers.get("Accept") == CONTENT_TYPE_JSON
assert aioclient_mock.mock_calls[-1][1].query["end"] == "5" assert last_request.headers.get("User-Agent") == "Mozilla/5.0"
assert last_request.url.params["start"] == "0"
assert last_request.url.params["end"] == "5"
assert_setup_component(1, SWITCH_DOMAIN) assert_setup_component(1, SWITCH_DOMAIN)
# Tests for REST switch platform. # Tests for REST switch platform.
async def _async_setup_test_switch( async def _async_setup_test_switch(hass: HomeAssistant) -> None:
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker respx.get(RESOURCE) % HTTPStatus.OK
) -> None:
aioclient_mock.get(RESOURCE, status=HTTPStatus.OK)
headers = {"Content-type": CONTENT_TYPE_JSON} headers = {"Content-type": CONTENT_TYPE_JSON}
config = { config = {
@@ -223,51 +220,48 @@ async def _async_setup_test_switch(
CONF_STATE_RESOURCE: STATE_RESOURCE, CONF_STATE_RESOURCE: STATE_RESOURCE,
CONF_HEADERS: headers, CONF_HEADERS: headers,
} }
assert await async_setup_component(hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: config}) assert await async_setup_component(hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: config})
await hass.async_block_till_done() await hass.async_block_till_done()
assert_setup_component(1, SWITCH_DOMAIN) assert_setup_component(1, SWITCH_DOMAIN)
assert hass.states.get("switch.foo").state == STATE_UNKNOWN assert hass.states.get("switch.foo").state == STATE_UNKNOWN
aioclient_mock.clear_requests() respx.reset()
async def test_name(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: @respx.mock
async def test_name(hass: HomeAssistant) -> None:
"""Test the name.""" """Test the name."""
await _async_setup_test_switch(hass, aioclient_mock) await _async_setup_test_switch(hass)
state = hass.states.get("switch.foo") state = hass.states.get("switch.foo")
assert state.attributes[ATTR_FRIENDLY_NAME] == NAME assert state.attributes[ATTR_FRIENDLY_NAME] == NAME
async def test_device_class( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_device_class(hass: HomeAssistant) -> None:
) -> None:
"""Test the device class.""" """Test the device class."""
await _async_setup_test_switch(hass, aioclient_mock) await _async_setup_test_switch(hass)
state = hass.states.get("switch.foo") state = hass.states.get("switch.foo")
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS
async def test_is_on_before_update( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_is_on_before_update(hass: HomeAssistant) -> None:
) -> None:
"""Test is_on in initial state.""" """Test is_on in initial state."""
await _async_setup_test_switch(hass, aioclient_mock) await _async_setup_test_switch(hass)
state = hass.states.get("switch.foo") state = hass.states.get("switch.foo")
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
async def test_turn_on_success( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_turn_on_success(hass: HomeAssistant) -> None:
) -> None:
"""Test turn_on.""" """Test turn_on."""
await _async_setup_test_switch(hass, aioclient_mock) await _async_setup_test_switch(hass)
aioclient_mock.post(RESOURCE, status=HTTPStatus.OK) route = respx.post(RESOURCE) % HTTPStatus.OK
aioclient_mock.get(RESOURCE, exc=aiohttp.ClientError) respx.get(RESOURCE).mock(side_effect=httpx.RequestError)
assert await hass.services.async_call( assert await hass.services.async_call(
SWITCH_DOMAIN, SWITCH_DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,
@@ -276,17 +270,18 @@ async def test_turn_on_success(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.mock_calls[-2][2].decode() == "ON" last_call = route.calls[-1]
last_request: httpx.Request = last_call.request
assert last_request.content.decode() == "ON"
assert hass.states.get("switch.foo").state == STATE_ON assert hass.states.get("switch.foo").state == STATE_ON
async def test_turn_on_status_not_ok( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_turn_on_status_not_ok(hass: HomeAssistant) -> None:
) -> None:
"""Test turn_on when error status returned.""" """Test turn_on when error status returned."""
await _async_setup_test_switch(hass, aioclient_mock) await _async_setup_test_switch(hass)
aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) route = respx.post(RESOURCE) % HTTPStatus.INTERNAL_SERVER_ERROR
assert await hass.services.async_call( assert await hass.services.async_call(
SWITCH_DOMAIN, SWITCH_DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,
@@ -295,17 +290,18 @@ async def test_turn_on_status_not_ok(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.mock_calls[-1][2].decode() == "ON" last_call = route.calls[-1]
last_request: httpx.Request = last_call.request
assert last_request.content.decode() == "ON"
assert hass.states.get("switch.foo").state == STATE_UNKNOWN assert hass.states.get("switch.foo").state == STATE_UNKNOWN
async def test_turn_on_timeout( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_turn_on_timeout(hass: HomeAssistant) -> None:
) -> None:
"""Test turn_on when timeout occurs.""" """Test turn_on when timeout occurs."""
await _async_setup_test_switch(hass, aioclient_mock) await _async_setup_test_switch(hass)
aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) respx.post(RESOURCE) % HTTPStatus.INTERNAL_SERVER_ERROR
assert await hass.services.async_call( assert await hass.services.async_call(
SWITCH_DOMAIN, SWITCH_DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,
@@ -317,14 +313,13 @@ async def test_turn_on_timeout(
assert hass.states.get("switch.foo").state == STATE_UNKNOWN assert hass.states.get("switch.foo").state == STATE_UNKNOWN
async def test_turn_off_success( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_turn_off_success(hass: HomeAssistant) -> None:
) -> None:
"""Test turn_off.""" """Test turn_off."""
await _async_setup_test_switch(hass, aioclient_mock) await _async_setup_test_switch(hass)
aioclient_mock.post(RESOURCE, status=HTTPStatus.OK) route = respx.post(RESOURCE) % HTTPStatus.OK
aioclient_mock.get(RESOURCE, exc=aiohttp.ClientError) respx.get(RESOURCE).mock(side_effect=httpx.RequestError)
assert await hass.services.async_call( assert await hass.services.async_call(
SWITCH_DOMAIN, SWITCH_DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
@@ -333,18 +328,19 @@ async def test_turn_off_success(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.mock_calls[-2][2].decode() == "OFF" last_call = route.calls[-1]
last_request: httpx.Request = last_call.request
assert last_request.content.decode() == "OFF"
assert hass.states.get("switch.foo").state == STATE_OFF assert hass.states.get("switch.foo").state == STATE_OFF
async def test_turn_off_status_not_ok( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_turn_off_status_not_ok(hass: HomeAssistant) -> None:
) -> None:
"""Test turn_off when error status returned.""" """Test turn_off when error status returned."""
await _async_setup_test_switch(hass, aioclient_mock) await _async_setup_test_switch(hass)
aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) route = respx.post(RESOURCE) % HTTPStatus.INTERNAL_SERVER_ERROR
assert await hass.services.async_call( assert await hass.services.async_call(
SWITCH_DOMAIN, SWITCH_DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
@@ -353,18 +349,19 @@ async def test_turn_off_status_not_ok(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert aioclient_mock.mock_calls[-1][2].decode() == "OFF" last_call = route.calls[-1]
last_request: httpx.Request = last_call.request
assert last_request.content.decode() == "OFF"
assert hass.states.get("switch.foo").state == STATE_UNKNOWN assert hass.states.get("switch.foo").state == STATE_UNKNOWN
async def test_turn_off_timeout( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_turn_off_timeout(hass: HomeAssistant) -> None:
) -> None:
"""Test turn_off when timeout occurs.""" """Test turn_off when timeout occurs."""
await _async_setup_test_switch(hass, aioclient_mock) await _async_setup_test_switch(hass)
aioclient_mock.post(RESOURCE, exc=asyncio.TimeoutError()) respx.post(RESOURCE).mock(side_effect=asyncio.TimeoutError())
assert await hass.services.async_call( assert await hass.services.async_call(
SWITCH_DOMAIN, SWITCH_DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
@@ -376,64 +373,59 @@ async def test_turn_off_timeout(
assert hass.states.get("switch.foo").state == STATE_UNKNOWN assert hass.states.get("switch.foo").state == STATE_UNKNOWN
async def test_update_when_on( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_update_when_on(hass: HomeAssistant) -> None:
) -> None:
"""Test update when switch is on.""" """Test update when switch is on."""
await _async_setup_test_switch(hass, aioclient_mock) await _async_setup_test_switch(hass)
aioclient_mock.get(RESOURCE, text="ON") respx.get(RESOURCE).respond(text="ON")
async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("switch.foo").state == STATE_ON assert hass.states.get("switch.foo").state == STATE_ON
async def test_update_when_off( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_update_when_off(hass: HomeAssistant) -> None:
) -> None:
"""Test update when switch is off.""" """Test update when switch is off."""
await _async_setup_test_switch(hass, aioclient_mock) await _async_setup_test_switch(hass)
aioclient_mock.get(RESOURCE, text="OFF") respx.get(RESOURCE).respond(text="OFF")
async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("switch.foo").state == STATE_OFF assert hass.states.get("switch.foo").state == STATE_OFF
async def test_update_when_unknown( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_update_when_unknown(hass: HomeAssistant) -> None:
) -> None:
"""Test update when unknown status returned.""" """Test update when unknown status returned."""
await _async_setup_test_switch(hass, aioclient_mock) await _async_setup_test_switch(hass)
aioclient_mock.get(RESOURCE, text="unknown status") respx.get(RESOURCE).respond(text="unknown status")
async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("switch.foo").state == STATE_UNKNOWN assert hass.states.get("switch.foo").state == STATE_UNKNOWN
async def test_update_timeout( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_update_timeout(hass: HomeAssistant) -> None:
) -> None:
"""Test update when timeout occurs.""" """Test update when timeout occurs."""
await _async_setup_test_switch(hass, aioclient_mock) await _async_setup_test_switch(hass)
aioclient_mock.get(RESOURCE, exc=asyncio.TimeoutError()) respx.get(RESOURCE).mock(side_effect=asyncio.TimeoutError())
async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("switch.foo").state == STATE_UNKNOWN assert hass.states.get("switch.foo").state == STATE_UNKNOWN
async def test_entity_config( @respx.mock
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker async def test_entity_config(hass: HomeAssistant) -> None:
) -> None:
"""Test entity configuration.""" """Test entity configuration."""
aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) respx.get(RESOURCE) % HTTPStatus.OK
config = { config = {
SWITCH_DOMAIN: { SWITCH_DOMAIN: {
# REST configuration # REST configuration

View File

@@ -102,7 +102,7 @@ INDEXED_SENSOR_CONFIG_2 = {
} }
NESTED_SENSOR_CONFIG = { NESTED_SENSOR_CONFIG_1 = {
"sn": { "sn": {
"Time": "2020-03-03T00:00:00+00:00", "Time": "2020-03-03T00:00:00+00:00",
"TX23": { "TX23": {
@@ -119,6 +119,17 @@ NESTED_SENSOR_CONFIG = {
} }
} }
NESTED_SENSOR_CONFIG_2 = {
"sn": {
"Time": "2023-01-27T11:04:56",
"DS18B20": {
"Id": "01191ED79190",
"Temperature": 2.4,
},
"TempUnit": "C",
}
}
async def test_controlling_state_via_mqtt( async def test_controlling_state_via_mqtt(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
@@ -174,12 +185,59 @@ async def test_controlling_state_via_mqtt(
assert state.state == "20.0" assert state.state == "20.0"
@pytest.mark.parametrize(
("sensor_config", "entity_ids", "messages", "states"),
[
(
NESTED_SENSOR_CONFIG_1,
["sensor.tasmota_tx23_speed_act", "sensor.tasmota_tx23_dir_card"],
(
'{"TX23":{"Speed":{"Act":"12.3"},"Dir": {"Card": "WSW"}}}',
'{"StatusSNS":{"TX23":{"Speed":{"Act":"23.4"},"Dir": {"Card": "ESE"}}}}',
),
(
{
"sensor.tasmota_tx23_speed_act": "12.3",
"sensor.tasmota_tx23_dir_card": "WSW",
},
{
"sensor.tasmota_tx23_speed_act": "23.4",
"sensor.tasmota_tx23_dir_card": "ESE",
},
),
),
(
NESTED_SENSOR_CONFIG_2,
["sensor.tasmota_ds18b20_temperature", "sensor.tasmota_ds18b20_id"],
(
'{"DS18B20":{"Id": "01191ED79190","Temperature": 12.3}}',
'{"StatusSNS":{"DS18B20":{"Id": "meep","Temperature": 23.4}}}',
),
(
{
"sensor.tasmota_ds18b20_temperature": "12.3",
"sensor.tasmota_ds18b20_id": "01191ED79190",
},
{
"sensor.tasmota_ds18b20_temperature": "23.4",
"sensor.tasmota_ds18b20_id": "meep",
},
),
),
],
)
async def test_nested_sensor_state_via_mqtt( async def test_nested_sensor_state_via_mqtt(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota hass: HomeAssistant,
mqtt_mock: MqttMockHAClient,
setup_tasmota,
sensor_config,
entity_ids,
messages,
states,
) -> None: ) -> None:
"""Test state update via MQTT.""" """Test state update via MQTT."""
config = copy.deepcopy(DEFAULT_CONFIG) config = copy.deepcopy(DEFAULT_CONFIG)
sensor_config = copy.deepcopy(NESTED_SENSOR_CONFIG) sensor_config = copy.deepcopy(sensor_config)
mac = config["mac"] mac = config["mac"]
async_fire_mqtt_message( async_fire_mqtt_message(
@@ -195,31 +253,29 @@ async def test_nested_sensor_state_via_mqtt(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("sensor.tasmota_tx23_speed_act") for entity_id in entity_ids:
assert state.state == "unavailable" state = hass.states.get(entity_id)
assert not state.attributes.get(ATTR_ASSUMED_STATE) assert state.state == "unavailable"
assert not state.attributes.get(ATTR_ASSUMED_STATE)
async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("sensor.tasmota_tx23_speed_act") for entity_id in entity_ids:
assert state.state == STATE_UNKNOWN state = hass.states.get(entity_id)
assert not state.attributes.get(ATTR_ASSUMED_STATE) assert state.state == STATE_UNKNOWN
assert not state.attributes.get(ATTR_ASSUMED_STATE)
# Test periodic state update # Test periodic state update
async_fire_mqtt_message( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", messages[0])
hass, "tasmota_49A3BC/tele/SENSOR", '{"TX23":{"Speed":{"Act":"12.3"}}}' for entity_id in entity_ids:
) state = hass.states.get(entity_id)
state = hass.states.get("sensor.tasmota_tx23_speed_act") assert state.state == states[0][entity_id]
assert state.state == "12.3"
# Test polled state update # Test polled state update
async_fire_mqtt_message( async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/STATUS10", messages[1])
hass, for entity_id in entity_ids:
"tasmota_49A3BC/stat/STATUS10", state = hass.states.get(entity_id)
'{"StatusSNS":{"TX23":{"Speed":{"Act":"23.4"}}}}', assert state.state == states[1][entity_id]
)
state = hass.states.get("sensor.tasmota_tx23_speed_act")
assert state.state == "23.4"
async def test_indexed_sensor_state_via_mqtt( async def test_indexed_sensor_state_via_mqtt(
@@ -728,7 +784,7 @@ async def test_nested_sensor_attributes(
) -> None: ) -> None:
"""Test correct attributes for sensors.""" """Test correct attributes for sensors."""
config = copy.deepcopy(DEFAULT_CONFIG) config = copy.deepcopy(DEFAULT_CONFIG)
sensor_config = copy.deepcopy(NESTED_SENSOR_CONFIG) sensor_config = copy.deepcopy(NESTED_SENSOR_CONFIG_1)
mac = config["mac"] mac = config["mac"]
async_fire_mqtt_message( async_fire_mqtt_message(
@@ -754,7 +810,7 @@ async def test_nested_sensor_attributes(
assert state.attributes.get("device_class") is None assert state.attributes.get("device_class") is None
assert state.attributes.get("friendly_name") == "Tasmota TX23 Dir Avg" assert state.attributes.get("friendly_name") == "Tasmota TX23 Dir Avg"
assert state.attributes.get("icon") is None assert state.attributes.get("icon") is None
assert state.attributes.get("unit_of_measurement") == " " assert state.attributes.get("unit_of_measurement") is None
async def test_indexed_sensor_attributes( async def test_indexed_sensor_attributes(

View File

@@ -2,7 +2,11 @@
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
from transmission_rpc.error import TransmissionError from transmission_rpc.error import (
TransmissionAuthError,
TransmissionConnectError,
TransmissionError,
)
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import transmission from homeassistant.components import transmission
@@ -125,7 +129,7 @@ async def test_error_on_wrong_credentials(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
mock_api.side_effect = TransmissionError("401: Unauthorized") mock_api.side_effect = TransmissionAuthError()
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
MOCK_CONFIG_DATA, MOCK_CONFIG_DATA,
@@ -137,6 +141,21 @@ async def test_error_on_wrong_credentials(
} }
async def test_unexpected_error(hass: HomeAssistant, mock_api: MagicMock) -> None:
"""Test we handle unexpected error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
mock_api.side_effect = TransmissionError()
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
MOCK_CONFIG_DATA,
)
assert result2["type"] == FlowResultType.FORM
assert result2["errors"] == {"base": "cannot_connect"}
async def test_error_on_connection_failure( async def test_error_on_connection_failure(
hass: HomeAssistant, mock_api: MagicMock hass: HomeAssistant, mock_api: MagicMock
) -> None: ) -> None:
@@ -145,7 +164,7 @@ async def test_error_on_connection_failure(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
mock_api.side_effect = TransmissionError("111: Connection refused") mock_api.side_effect = TransmissionConnectError()
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
MOCK_CONFIG_DATA, MOCK_CONFIG_DATA,
@@ -213,7 +232,7 @@ async def test_reauth_failed(hass: HomeAssistant, mock_api: MagicMock) -> None:
assert result["step_id"] == "reauth_confirm" assert result["step_id"] == "reauth_confirm"
assert result["description_placeholders"] == {"username": "user"} assert result["description_placeholders"] == {"username": "user"}
mock_api.side_effect = TransmissionError("401: Unauthorized") mock_api.side_effect = TransmissionAuthError()
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {
@@ -248,7 +267,7 @@ async def test_reauth_failed_connection_error(
assert result["step_id"] == "reauth_confirm" assert result["step_id"] == "reauth_confirm"
assert result["description_placeholders"] == {"username": "user"} assert result["description_placeholders"] == {"username": "user"}
mock_api.side_effect = TransmissionError("111: Connection refused") mock_api.side_effect = TransmissionConnectError()
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {

View File

@@ -3,7 +3,11 @@
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
from transmission_rpc.error import TransmissionError from transmission_rpc.error import (
TransmissionAuthError,
TransmissionConnectError,
TransmissionError,
)
from homeassistant.components.transmission.const import DOMAIN from homeassistant.components.transmission.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
@@ -40,7 +44,7 @@ async def test_setup_failed_connection_error(
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA)
entry.add_to_hass(hass) entry.add_to_hass(hass)
mock_api.side_effect = TransmissionError("111: Connection refused") mock_api.side_effect = TransmissionConnectError()
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)
assert entry.state == ConfigEntryState.SETUP_RETRY assert entry.state == ConfigEntryState.SETUP_RETRY
@@ -54,7 +58,21 @@ async def test_setup_failed_auth_error(
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA)
entry.add_to_hass(hass) entry.add_to_hass(hass)
mock_api.side_effect = TransmissionError("401: Unauthorized") mock_api.side_effect = TransmissionAuthError()
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state == ConfigEntryState.SETUP_ERROR
async def test_setup_failed_unexpected_error(
hass: HomeAssistant, mock_api: MagicMock
) -> None:
"""Test integration failed due to unexpected error."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA)
entry.add_to_hass(hass)
mock_api.side_effect = TransmissionError()
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)
assert entry.state == ConfigEntryState.SETUP_ERROR assert entry.state == ConfigEntryState.SETUP_ERROR

View File

@@ -1,11 +1,13 @@
"""Test ZHA Core cluster handlers.""" """Test ZHA Core cluster handlers."""
import asyncio import asyncio
from collections.abc import Callable from collections.abc import Callable
import logging
import math import math
from unittest import mock from unittest import mock
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
import zigpy.device
import zigpy.endpoint import zigpy.endpoint
from zigpy.endpoint import Endpoint as ZigpyEndpoint from zigpy.endpoint import Endpoint as ZigpyEndpoint
import zigpy.profiles.zha import zigpy.profiles.zha
@@ -791,3 +793,41 @@ async def test_configure_reporting(hass: HomeAssistant, endpoint) -> None:
} }
), ),
] ]
async def test_invalid_cluster_handler(hass: HomeAssistant, caplog) -> None:
"""Test setting up a cluster handler that fails to match properly."""
class TestZigbeeClusterHandler(cluster_handlers.ClusterHandler):
REPORT_CONFIG = (
cluster_handlers.AttrReportConfig(attr="missing_attr", config=(1, 60, 1)),
)
mock_device = mock.AsyncMock(spec_set=zigpy.device.Device)
zigpy_ep = zigpy.endpoint.Endpoint(mock_device, endpoint_id=1)
cluster = zigpy_ep.add_input_cluster(zigpy.zcl.clusters.lighting.Color.cluster_id)
cluster.configure_reporting_multiple = AsyncMock(
spec_set=cluster.configure_reporting_multiple,
return_value=[
foundation.ConfigureReportingResponseRecord(
status=foundation.Status.SUCCESS
)
],
)
mock_zha_device = mock.AsyncMock(spec_set=ZHADevice)
zha_endpoint = Endpoint(zigpy_ep, mock_zha_device)
# The cluster handler throws an error when matching this cluster
with pytest.raises(KeyError):
TestZigbeeClusterHandler(cluster, zha_endpoint)
# And one is also logged at runtime
with patch.dict(
registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY,
{cluster.cluster_id: TestZigbeeClusterHandler},
), caplog.at_level(logging.WARNING):
zha_endpoint.add_all_cluster_handlers()
assert "missing_attr" in caplog.text