Merge branch 'dev' into onvif_renew

This commit is contained in:
J. Nick Koston
2023-05-05 08:03:10 -05:00
committed by GitHub
30 changed files with 609 additions and 230 deletions

View File

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

View File

@@ -199,14 +199,10 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
# Don't migrate if there's a YAML config
return
for state in self.hass.states.async_all():
async_expose_entity(
self.hass,
CLOUD_ALEXA,
state.entity_id,
self._should_expose_legacy(state.entity_id),
)
for entity_id in self._prefs.alexa_entity_configs:
for entity_id in {
*self.hass.states.async_entity_ids(),
*self._prefs.alexa_entity_configs,
}:
async_expose_entity(
self.hass,
CLOUD_ALEXA,
@@ -220,8 +216,18 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
async def on_hass_started(hass):
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()
await self._prefs.async_update(
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.homeassistant.exposed_entities import (
async_expose_entity,
async_get_assistant_settings,
async_get_entity_settings,
async_listen_entity_updates,
async_set_assistant_option,
@@ -175,23 +176,10 @@ class CloudGoogleConfig(AbstractConfig):
# Don't migrate if there's a YAML config
return
for state in self.hass.states.async_all():
entity_id = state.entity_id
async_expose_entity(
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:
for entity_id in {
*self.hass.states.async_entity_ids(),
*self._prefs.google_entity_configs,
}:
async_expose_entity(
self.hass,
CLOUD_GOOGLE,
@@ -213,8 +201,18 @@ class CloudGoogleConfig(AbstractConfig):
async def on_hass_started(hass: HomeAssistant) -> None:
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()
await self._prefs.async_update(
google_settings_version=GOOGLE_SETTINGS_VERSION
)

View File

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

View File

@@ -15,5 +15,5 @@
"documentation": "https://www.home-assistant.io/integrations/elkm1",
"iot_class": "local_push",
"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,
SelectInfo,
SensorInfo,
SensorState,
SwitchInfo,
TextSensorInfo,
UserService,
@@ -240,9 +241,18 @@ class RuntimeEntryData:
current_state_by_type = self.state[state_type]
current_state = current_state_by_type.get(key, _SENTINEL)
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(
"%s: ignoring duplicate update with and key %s: %s",
"%s: ignoring duplicate update with key %s: %s",
self.name,
key,
state,

View File

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

View File

@@ -1,6 +1,6 @@
"""Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org."""
import asyncio
from datetime import timedelta
from datetime import datetime, timedelta
import logging
import aiohttp
@@ -53,11 +53,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if result is False:
return False
async def update_domain_callback(now):
async def update_domain_callback(now: datetime) -> None:
"""Update the FreeDNS entry."""
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

View File

@@ -590,7 +590,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
await async_setup_addon_panel(hass, hassio)
# 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."""
if (os_info := get_os_info(hass)) is None:
# 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.config_entries.flow.async_init(DOMAIN, context={"source": "system"})

View File

@@ -31,7 +31,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
from homeassistant.helpers import discovery, event
import homeassistant.helpers.config_validation as cv
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):
_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:
_LOGGER.info("Adapter not initialized; Trying to restart")
hass.bus.fire(EVENT_HDMI_CEC_UNAVAILABLE)
adapter.init()
_adapter_watchdog_job = HassJob(_adapter_watchdog, cancel_on_shutdown=True)
@callback
def _async_initialized_callback(*_: Any):
"""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)

View File

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

View File

@@ -50,6 +50,11 @@ class RestData:
self.last_exception: Exception | 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:
"""Set url."""
self._resource = url

View File

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

View File

@@ -6,8 +6,8 @@ from http import HTTPStatus
import logging
from typing import Any
import aiohttp
import async_timeout
import httpx
import voluptuous as vol
from homeassistant.components.switch import (
@@ -30,8 +30,8 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
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.httpx_client import get_async_client
from homeassistant.helpers.template_entity import (
TEMPLATE_ENTITY_BASE_SCHEMA,
TemplateEntity,
@@ -89,8 +89,8 @@ async def async_setup_platform(
switch = RestSwitch(hass, config, unique_id)
req = await switch.get_device_state(hass)
if req.status >= HTTPStatus.BAD_REQUEST:
_LOGGER.error("Got non-ok response from resource: %s", req.status)
if req.status_code >= HTTPStatus.BAD_REQUEST:
_LOGGER.error("Got non-ok response from resource: %s", req.status_code)
else:
async_add_entities([switch])
except (TypeError, ValueError):
@@ -98,7 +98,7 @@ async def async_setup_platform(
"Missing resource or schema in configuration. "
"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
@@ -120,11 +120,11 @@ class RestSwitch(TemplateEntity, SwitchEntity):
unique_id=unique_id,
)
auth: aiohttp.BasicAuth | None = None
auth: httpx.BasicAuth | None = None
username: str | None = None
if username := config.get(CONF_USERNAME):
password: str = config[CONF_PASSWORD]
auth = aiohttp.BasicAuth(username, password=password)
auth = httpx.BasicAuth(username, password=password)
self._resource: str = config[CONF_RESOURCE]
self._state_resource: str = config.get(CONF_STATE_RESOURCE) or self._resource
@@ -155,13 +155,13 @@ class RestSwitch(TemplateEntity, SwitchEntity):
try:
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
else:
_LOGGER.error(
"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)
async def async_turn_off(self, **kwargs: Any) -> None:
@@ -170,24 +170,24 @@ class RestSwitch(TemplateEntity, SwitchEntity):
try:
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
else:
_LOGGER.error(
"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)
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."""
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_params = template.render_complex(self._params)
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,
auth=self._auth,
data=bytes(body, "utf-8"),
@@ -202,12 +202,12 @@ class RestSwitch(TemplateEntity, SwitchEntity):
await self.get_device_state(self.hass)
except asyncio.TimeoutError:
_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)
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."""
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_params = template.render_complex(self._params)
@@ -219,7 +219,7 @@ class RestSwitch(TemplateEntity, SwitchEntity):
headers=rendered_headers,
params=rendered_params,
)
text = await req.text()
text = req.text
if self._is_on_template is not None:
text = self._is_on_template.async_render_with_possible_json_value(

View File

@@ -123,7 +123,7 @@ class SIAAlarmControlPanel(SIABaseEntity, AlarmControlPanelEntity):
"""
new_state = None
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:
return False
_LOGGER.debug("New state will be %s", new_state)

View File

@@ -132,7 +132,7 @@ class SIABinarySensor(SIABaseEntity, BinarySensorEntity):
"""
new_state = None
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:
return False
_LOGGER.debug("New state will be %s", new_state)

View File

@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["hatasmota"],
"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.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
TextSelector,
)
import homeassistant.util.dt as dt_util
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(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_FROM): cv.string,
vol.Required(CONF_TO): cv.string,
vol.Optional(CONF_TIME): cv.string,
vol.Required(CONF_WEEKDAY, default=WEEKDAYS): cv.multi_select(
{day: day for day in WEEKDAYS}
vol.Required(CONF_API_KEY): TextSelector(),
vol.Required(CONF_FROM): TextSelector(),
vol.Required(CONF_TO): TextSelector(),
vol.Optional(CONF_TIME): TextSelector(),
vol.Required(CONF_WEEKDAY, default=WEEKDAYS): SelectSelector(
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
import transmission_rpc
from transmission_rpc.error import TransmissionError
from transmission_rpc.error import (
TransmissionAuthError,
TransmissionConnectError,
TransmissionError,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
@@ -137,14 +141,13 @@ async def get_api(hass, entry):
_LOGGER.debug("Successfully connected to %s", host)
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:
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)
raise UnknownError from error

View File

@@ -137,7 +137,19 @@ class Endpoint:
):
cluster_handler_class = MultistateInput
# 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:
self._device.power_configuration_ch = cluster_handler
elif cluster_handler.name == const.CLUSTER_HANDLER_IDENTIFY:

View File

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

View File

@@ -146,7 +146,7 @@ aioecowitt==2023.01.0
aioemonitor==1.0.5
# homeassistant.components.esphome
aioesphomeapi==13.7.2
aioesphomeapi==13.7.3
# homeassistant.components.flo
aioflo==2021.11.0
@@ -506,7 +506,7 @@ easyenergy==0.3.0
elgato==4.0.1
# homeassistant.components.elkm1
elkm1-lib==2.2.1
elkm1-lib==2.2.2
# homeassistant.components.elmax
elmax_api==0.0.4
@@ -679,7 +679,7 @@ hass-nabucasa==0.66.2
hassil==1.0.6
# homeassistant.components.tasmota
hatasmota==0.6.4
hatasmota==0.6.5
# homeassistant.components.jewish_calendar
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
@pytest.mark.parametrize("alexa_settings_version", [1, 2])
async def test_alexa_config_migrate_expose_entity_prefs(
hass: HomeAssistant,
cloud_prefs: CloudPreferences,
cloud_stub,
entity_registry: er.EntityRegistry,
alexa_settings_version: int,
) -> None:
"""Test migrating Alexa entity config."""
hass.state = CoreState.starting
@@ -593,7 +595,7 @@ async def test_alexa_config_migrate_expose_entity_prefs(
await cloud_prefs.async_update(
alexa_enabled=True,
alexa_report_state=False,
alexa_settings_version=1,
alexa_settings_version=alexa_settings_version,
)
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(
hass: HomeAssistant,
cloud_prefs: CloudPreferences,

View File

@@ -483,10 +483,12 @@ async def test_google_handle_logout(
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(
hass: HomeAssistant,
cloud_prefs: CloudPreferences,
entity_registry: er.EntityRegistry,
google_settings_version: int,
) -> None:
"""Test migrating Google entity config."""
hass.state = CoreState.starting
@@ -540,7 +542,7 @@ async def test_google_config_migrate_expose_entity_prefs(
await cloud_prefs.async_update(
google_enabled=True,
google_report_state=False,
google_settings_version=1,
google_settings_version=google_settings_version,
)
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(
hass: HomeAssistant,
cloud_prefs: CloudPreferences,

View File

@@ -2,8 +2,9 @@
import asyncio
from http import HTTPStatus
import aiohttp
import httpx
import pytest
import respx
from homeassistant.components.rest import DOMAIN
from homeassistant.components.rest.switch import (
@@ -45,7 +46,6 @@ from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from tests.common import assert_setup_component, async_fire_time_changed
from tests.test_util.aiohttp import AiohttpClientMocker
NAME = "foo"
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
@respx.mock
async def test_setup_failed_connect(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
caplog: pytest.LogCaptureFixture,
) -> None:
"""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}}
assert await async_setup_component(hass, SWITCH_DOMAIN, config)
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
@respx.mock
async def test_setup_timeout(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
caplog: pytest.LogCaptureFixture,
) -> None:
"""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}}
assert await async_setup_component(hass, SWITCH_DOMAIN, config)
await hass.async_block_till_done()
@@ -103,23 +103,21 @@ async def test_setup_timeout(
assert "No route to resource/endpoint" in caplog.text
async def test_setup_minimum(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_setup_minimum(hass: HomeAssistant) -> None:
"""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}}
with assert_setup_component(1, SWITCH_DOMAIN):
assert await async_setup_component(hass, SWITCH_DOMAIN, config)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 1
assert route.call_count == 1
async def test_setup_query_params(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_setup_query_params(hass: HomeAssistant) -> None:
"""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 = {
SWITCH_DOMAIN: {
CONF_PLATFORM: DOMAIN,
@@ -131,12 +129,13 @@ async def test_setup_query_params(
assert await async_setup_component(hass, SWITCH_DOMAIN, config)
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."""
aioclient_mock.get(RESOURCE, status=HTTPStatus.OK)
route = respx.get(RESOURCE) % HTTPStatus.OK
config = {
SWITCH_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)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 1
assert route.call_count == 1
assert_setup_component(1, SWITCH_DOMAIN)
async def test_setup_with_state_resource(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_setup_with_state_resource(hass: HomeAssistant) -> None:
"""Test setup with valid configuration."""
aioclient_mock.get(RESOURCE, status=HTTPStatus.NOT_FOUND)
aioclient_mock.get("http://localhost/state", status=HTTPStatus.OK)
respx.get(RESOURCE) % HTTPStatus.NOT_FOUND
route = respx.get("http://localhost/state") % HTTPStatus.OK
config = {
SWITCH_DOMAIN: {
CONF_PLATFORM: DOMAIN,
@@ -172,15 +170,14 @@ async def test_setup_with_state_resource(
}
assert await async_setup_component(hass, SWITCH_DOMAIN, config)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 1
assert route.call_count == 1
assert_setup_component(1, SWITCH_DOMAIN)
async def test_setup_with_templated_headers_params(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_setup_with_templated_headers_params(hass: HomeAssistant) -> None:
"""Test setup with valid configuration."""
aioclient_mock.get(RESOURCE, status=HTTPStatus.OK)
route = respx.get(RESOURCE) % HTTPStatus.OK
config = {
SWITCH_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)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 1
assert aioclient_mock.mock_calls[-1][3].get("Accept") == CONTENT_TYPE_JSON
assert aioclient_mock.mock_calls[-1][3].get("User-Agent") == "Mozilla/5.0"
assert aioclient_mock.mock_calls[-1][1].query["start"] == "0"
assert aioclient_mock.mock_calls[-1][1].query["end"] == "5"
assert route.call_count == 1
last_call = route.calls[-1]
last_request: httpx.Request = last_call.request
assert last_request.headers.get("Accept") == CONTENT_TYPE_JSON
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)
# Tests for REST switch platform.
async def _async_setup_test_switch(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
aioclient_mock.get(RESOURCE, status=HTTPStatus.OK)
async def _async_setup_test_switch(hass: HomeAssistant) -> None:
respx.get(RESOURCE) % HTTPStatus.OK
headers = {"Content-type": CONTENT_TYPE_JSON}
config = {
@@ -223,51 +220,48 @@ async def _async_setup_test_switch(
CONF_STATE_RESOURCE: STATE_RESOURCE,
CONF_HEADERS: headers,
}
assert await async_setup_component(hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: config})
await hass.async_block_till_done()
assert_setup_component(1, SWITCH_DOMAIN)
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."""
await _async_setup_test_switch(hass, aioclient_mock)
await _async_setup_test_switch(hass)
state = hass.states.get("switch.foo")
assert state.attributes[ATTR_FRIENDLY_NAME] == NAME
async def test_device_class(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_device_class(hass: HomeAssistant) -> None:
"""Test the device class."""
await _async_setup_test_switch(hass, aioclient_mock)
await _async_setup_test_switch(hass)
state = hass.states.get("switch.foo")
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS
async def test_is_on_before_update(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_is_on_before_update(hass: HomeAssistant) -> None:
"""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")
assert state.state == STATE_UNKNOWN
async def test_turn_on_success(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_turn_on_success(hass: HomeAssistant) -> None:
"""Test turn_on."""
await _async_setup_test_switch(hass, aioclient_mock)
await _async_setup_test_switch(hass)
aioclient_mock.post(RESOURCE, status=HTTPStatus.OK)
aioclient_mock.get(RESOURCE, exc=aiohttp.ClientError)
route = respx.post(RESOURCE) % HTTPStatus.OK
respx.get(RESOURCE).mock(side_effect=httpx.RequestError)
assert await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
@@ -276,17 +270,18 @@ async def test_turn_on_success(
)
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
async def test_turn_on_status_not_ok(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_turn_on_status_not_ok(hass: HomeAssistant) -> None:
"""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(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
@@ -295,17 +290,18 @@ async def test_turn_on_status_not_ok(
)
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
async def test_turn_on_timeout(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_turn_on_timeout(hass: HomeAssistant) -> None:
"""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(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
@@ -317,14 +313,13 @@ async def test_turn_on_timeout(
assert hass.states.get("switch.foo").state == STATE_UNKNOWN
async def test_turn_off_success(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_turn_off_success(hass: HomeAssistant) -> None:
"""Test turn_off."""
await _async_setup_test_switch(hass, aioclient_mock)
await _async_setup_test_switch(hass)
aioclient_mock.post(RESOURCE, status=HTTPStatus.OK)
aioclient_mock.get(RESOURCE, exc=aiohttp.ClientError)
route = respx.post(RESOURCE) % HTTPStatus.OK
respx.get(RESOURCE).mock(side_effect=httpx.RequestError)
assert await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
@@ -333,18 +328,19 @@ async def test_turn_off_success(
)
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
async def test_turn_off_status_not_ok(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_turn_off_status_not_ok(hass: HomeAssistant) -> None:
"""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(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
@@ -353,18 +349,19 @@ async def test_turn_off_status_not_ok(
)
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
async def test_turn_off_timeout(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_turn_off_timeout(hass: HomeAssistant) -> None:
"""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(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
@@ -376,64 +373,59 @@ async def test_turn_off_timeout(
assert hass.states.get("switch.foo").state == STATE_UNKNOWN
async def test_update_when_on(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_update_when_on(hass: HomeAssistant) -> None:
"""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)
await hass.async_block_till_done()
assert hass.states.get("switch.foo").state == STATE_ON
async def test_update_when_off(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_update_when_off(hass: HomeAssistant) -> None:
"""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)
await hass.async_block_till_done()
assert hass.states.get("switch.foo").state == STATE_OFF
async def test_update_when_unknown(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_update_when_unknown(hass: HomeAssistant) -> None:
"""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)
await hass.async_block_till_done()
assert hass.states.get("switch.foo").state == STATE_UNKNOWN
async def test_update_timeout(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_update_timeout(hass: HomeAssistant) -> None:
"""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)
await hass.async_block_till_done()
assert hass.states.get("switch.foo").state == STATE_UNKNOWN
async def test_entity_config(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
@respx.mock
async def test_entity_config(hass: HomeAssistant) -> None:
"""Test entity configuration."""
aioclient_mock.get(RESOURCE, status=HTTPStatus.OK)
respx.get(RESOURCE) % HTTPStatus.OK
config = {
SWITCH_DOMAIN: {
# REST configuration

View File

@@ -102,7 +102,7 @@ INDEXED_SENSOR_CONFIG_2 = {
}
NESTED_SENSOR_CONFIG = {
NESTED_SENSOR_CONFIG_1 = {
"sn": {
"Time": "2020-03-03T00:00:00+00:00",
"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(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
@@ -174,12 +185,59 @@ async def test_controlling_state_via_mqtt(
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(
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota
hass: HomeAssistant,
mqtt_mock: MqttMockHAClient,
setup_tasmota,
sensor_config,
entity_ids,
messages,
states,
) -> None:
"""Test state update via MQTT."""
config = copy.deepcopy(DEFAULT_CONFIG)
sensor_config = copy.deepcopy(NESTED_SENSOR_CONFIG)
sensor_config = copy.deepcopy(sensor_config)
mac = config["mac"]
async_fire_mqtt_message(
@@ -195,31 +253,29 @@ async def test_nested_sensor_state_via_mqtt(
)
await hass.async_block_till_done()
state = hass.states.get("sensor.tasmota_tx23_speed_act")
assert state.state == "unavailable"
assert not state.attributes.get(ATTR_ASSUMED_STATE)
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state.state == "unavailable"
assert not state.attributes.get(ATTR_ASSUMED_STATE)
async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
await hass.async_block_till_done()
state = hass.states.get("sensor.tasmota_tx23_speed_act")
assert state.state == STATE_UNKNOWN
assert not state.attributes.get(ATTR_ASSUMED_STATE)
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state.state == STATE_UNKNOWN
assert not state.attributes.get(ATTR_ASSUMED_STATE)
# Test periodic state update
async_fire_mqtt_message(
hass, "tasmota_49A3BC/tele/SENSOR", '{"TX23":{"Speed":{"Act":"12.3"}}}'
)
state = hass.states.get("sensor.tasmota_tx23_speed_act")
assert state.state == "12.3"
async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", messages[0])
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state.state == states[0][entity_id]
# Test polled state update
async_fire_mqtt_message(
hass,
"tasmota_49A3BC/stat/STATUS10",
'{"StatusSNS":{"TX23":{"Speed":{"Act":"23.4"}}}}',
)
state = hass.states.get("sensor.tasmota_tx23_speed_act")
assert state.state == "23.4"
async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/STATUS10", messages[1])
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state.state == states[1][entity_id]
async def test_indexed_sensor_state_via_mqtt(
@@ -728,7 +784,7 @@ async def test_nested_sensor_attributes(
) -> None:
"""Test correct attributes for sensors."""
config = copy.deepcopy(DEFAULT_CONFIG)
sensor_config = copy.deepcopy(NESTED_SENSOR_CONFIG)
sensor_config = copy.deepcopy(NESTED_SENSOR_CONFIG_1)
mac = config["mac"]
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("friendly_name") == "Tasmota TX23 Dir Avg"
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(

View File

@@ -2,7 +2,11 @@
from unittest.mock import MagicMock, patch
import pytest
from transmission_rpc.error import TransmissionError
from transmission_rpc.error import (
TransmissionAuthError,
TransmissionConnectError,
TransmissionError,
)
from homeassistant import config_entries
from homeassistant.components import transmission
@@ -125,7 +129,7 @@ async def test_error_on_wrong_credentials(
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(
result["flow_id"],
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(
hass: HomeAssistant, mock_api: MagicMock
) -> None:
@@ -145,7 +164,7 @@ async def test_error_on_connection_failure(
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(
result["flow_id"],
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["description_placeholders"] == {"username": "user"}
mock_api.side_effect = TransmissionError("401: Unauthorized")
mock_api.side_effect = TransmissionAuthError()
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
@@ -248,7 +267,7 @@ async def test_reauth_failed_connection_error(
assert result["step_id"] == "reauth_confirm"
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(
result["flow_id"],
{

View File

@@ -3,7 +3,11 @@
from unittest.mock import MagicMock, patch
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.config_entries import ConfigEntryState
@@ -40,7 +44,7 @@ async def test_setup_failed_connection_error(
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA)
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)
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.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)
assert entry.state == ConfigEntryState.SETUP_ERROR

View File

@@ -1,11 +1,13 @@
"""Test ZHA Core cluster handlers."""
import asyncio
from collections.abc import Callable
import logging
import math
from unittest import mock
from unittest.mock import AsyncMock, patch
import pytest
import zigpy.device
import zigpy.endpoint
from zigpy.endpoint import Endpoint as ZigpyEndpoint
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