diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 7fa165ebaefbee..82a232fa7cf3aa 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "platinum", - "requirements": ["aioamazondevices==10.0.0"] + "requirements": ["aioamazondevices==11.0.2"] } diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index f0917c769bfe78..6a354458ed32f7 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -24,7 +24,7 @@ CONF_SSL, CONF_USERNAME, ) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.device_registry import DeviceInfo @@ -227,7 +227,10 @@ async def async_added_to_hass(self) -> None: # Register callback with pyhik self._camera.add_update_callback(self._update_callback, self._callback_id) - @callback def _update_callback(self, msg: str) -> None: - """Update the sensor's state when callback is triggered.""" - self.async_write_ha_state() + """Update the sensor's state when callback is triggered. + + This is called from pyhik's event stream thread, so we use + schedule_update_ha_state which is thread-safe. + """ + self.schedule_update_ha_state() diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index 3a04a587bb4eda..19fcd0295a2cb8 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -67,21 +67,21 @@ def _get_water_use(sensor: HydrawiseSensor) -> ControllerWaterUseSummary: HydrawiseSensorEntityDescription( key="daily_total_water_use", translation_key="daily_total_water_use", - device_class=SensorDeviceClass.VOLUME, + device_class=SensorDeviceClass.WATER, suggested_display_precision=1, value_fn=lambda sensor: _get_water_use(sensor).total_use, ), HydrawiseSensorEntityDescription( key="daily_active_water_use", translation_key="daily_active_water_use", - device_class=SensorDeviceClass.VOLUME, + device_class=SensorDeviceClass.WATER, suggested_display_precision=1, value_fn=lambda sensor: _get_water_use(sensor).total_active_use, ), HydrawiseSensorEntityDescription( key="daily_inactive_water_use", translation_key="daily_inactive_water_use", - device_class=SensorDeviceClass.VOLUME, + device_class=SensorDeviceClass.WATER, suggested_display_precision=1, value_fn=lambda sensor: _get_water_use(sensor).total_inactive_use, ), @@ -91,7 +91,7 @@ def _get_water_use(sensor: HydrawiseSensor) -> ControllerWaterUseSummary: HydrawiseSensorEntityDescription( key="daily_active_water_use", translation_key="daily_active_water_use", - device_class=SensorDeviceClass.VOLUME, + device_class=SensorDeviceClass.WATER, suggested_display_precision=1, value_fn=lambda sensor: float( _get_water_use(sensor).active_use_by_zone_id.get(sensor.zone.id, 0.0) @@ -204,7 +204,7 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity): @property def native_unit_of_measurement(self) -> str | None: """Return the unit_of_measurement of the sensor.""" - if self.entity_description.device_class != SensorDeviceClass.VOLUME: + if self.entity_description.device_class != SensorDeviceClass.WATER: return self.entity_description.native_unit_of_measurement return ( UnitOfVolume.GALLONS @@ -217,7 +217,7 @@ def icon(self) -> str | None: """Icon of the entity based on the value.""" if ( self.entity_description.key in FLOW_MEASUREMENT_KEYS - and self.entity_description.device_class == SensorDeviceClass.VOLUME + and self.entity_description.device_class == SensorDeviceClass.WATER and round(self.state, 2) == 0.0 ): return "mdi:water-outline" diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index a6df67a7c83b6d..a2c6338f21cfc0 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -51,7 +51,6 @@ PLATFORMS = [ Platform.BINARY_SENSOR, - Platform.NOTIFY, Platform.SENSOR, ] @@ -61,6 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Netgear LTE component.""" hass.data[DATA_HASS_CONFIG] = config + async_setup_services(hass) return True @@ -96,19 +96,15 @@ def fire_sms_event(sms: SMS) -> None: await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator - async_setup_services(hass) - await discovery.async_load_platform( hass, Platform.NOTIFY, DOMAIN, - {CONF_NAME: entry.title, "modem": modem}, + {CONF_NAME: entry.title, "modem": modem, "entry": entry}, hass.data[DATA_HASS_CONFIG], ) - await hass.config_entries.async_forward_entry_setups( - entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] - ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -118,7 +114,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: NetgearLTEConfigEntry) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if not hass.config_entries.async_loaded_entries(DOMAIN): hass.data.pop(DOMAIN, None) - for service_name in hass.services.async_services()[DOMAIN]: - hass.services.async_remove(DOMAIN, service_name) return unload_ok diff --git a/homeassistant/components/netgear_lte/config_flow.py b/homeassistant/components/netgear_lte/config_flow.py index 0b8f68246ca910..8eacb693089927 100644 --- a/homeassistant/components/netgear_lte/config_flow.py +++ b/homeassistant/components/netgear_lte/config_flow.py @@ -14,7 +14,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_create_clientsession -from .const import DEFAULT_HOST, DOMAIN, LOGGER, MANUFACTURER +from .const import DEFAULT_HOST, DOMAIN, MANUFACTURER class NetgearLTEFlowHandler(ConfigFlow, domain=DOMAIN): @@ -72,9 +72,6 @@ async def _async_validate_input(self, host: str, password: str) -> Information: info = await modem.information() except Error as ex: raise InputValidationError("cannot_connect") from ex - except Exception as ex: - LOGGER.exception("Unexpected exception") - raise InputValidationError("unknown") from ex await modem.logout() return info diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 4ab535f8bd8cfb..72fa7689edba73 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["eternalegypt"], - "requirements": ["eternalegypt==0.0.16"] + "requirements": ["eternalegypt==0.0.18"] } diff --git a/homeassistant/components/netgear_lte/notify.py b/homeassistant/components/netgear_lte/notify.py index 763581b9cadae6..5b10398e05565c 100644 --- a/homeassistant/components/netgear_lte/notify.py +++ b/homeassistant/components/netgear_lte/notify.py @@ -38,6 +38,7 @@ def __init__( """Initialize the service.""" self.config = config self.modem: Modem = discovery_info["modem"] + discovery_info["entry"].async_on_unload(self.async_unregister_services) async def async_send_message(self, message="", **kwargs): """Send a message to a user.""" diff --git a/homeassistant/components/netgear_lte/services.py b/homeassistant/components/netgear_lte/services.py index 5cac48c26343c0..45b3e41189aafa 100644 --- a/homeassistant/components/netgear_lte/services.py +++ b/homeassistant/components/netgear_lte/services.py @@ -4,6 +4,7 @@ from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv from .const import ( @@ -14,7 +15,6 @@ AUTOCONNECT_MODES, DOMAIN, FAILOVER_MODES, - LOGGER, ) from .coordinator import NetgearLTEConfigEntry @@ -56,8 +56,11 @@ async def _service_handler(call: ServiceCall) -> None: break if not entry or not (modem := entry.runtime_data.modem).token: - LOGGER.error("%s: host %s unavailable", call.service, host) - return + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="config_entry_not_found", + translation_placeholders={"service": call.service}, + ) if call.service == SERVICE_DELETE_SMS: for sms_id in call.data[ATTR_SMS_ID]: diff --git a/homeassistant/components/netgear_lte/strings.json b/homeassistant/components/netgear_lte/strings.json index 7db66f6fab6e13..37042e268a1e0d 100644 --- a/homeassistant/components/netgear_lte/strings.json +++ b/homeassistant/components/netgear_lte/strings.json @@ -71,6 +71,11 @@ } } }, + "exceptions": { + "config_entry_not_found": { + "message": "Failed to perform action \"{service}\". Config entry for target not found" + } + }, "services": { "connect_lte": { "description": "Asks the modem to establish the LTE connection.", diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 146a18555e2752..beac2ffc3437fe 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -12,7 +12,7 @@ "quality_scale": "bronze", "requirements": [ "defusedxml==0.7.1", - "soco==0.30.13", + "soco==0.30.14", "sonos-websocket==0.1.3" ], "ssdp": [ diff --git a/homeassistant/core.py b/homeassistant/core.py index 917d86d752cd40..1bc9299c85d02d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -2412,10 +2412,8 @@ class Service: __slots__ = [ "description_placeholders", - "domain", "job", "schema", - "service", "supports_response", ] diff --git a/requirements_all.txt b/requirements_all.txt index f06888e1c0c216..835941207b96d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -190,7 +190,7 @@ aioairzone-cloud==0.7.2 aioairzone==1.0.4 # homeassistant.components.alexa_devices -aioamazondevices==10.0.0 +aioamazondevices==11.0.2 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -929,7 +929,7 @@ esphome-dashboard-api==1.3.0 essent-dynamic-pricing==0.2.7 # homeassistant.components.netgear_lte -eternalegypt==0.0.16 +eternalegypt==0.0.18 # homeassistant.components.eufylife_ble eufylife-ble-client==0.1.8 @@ -2887,7 +2887,7 @@ smart-meter-texas==0.5.5 snapcast==2.3.6 # homeassistant.components.sonos -soco==0.30.13 +soco==0.30.14 # homeassistant.components.solaredge_local solaredge-local==0.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20cc5b42c5ac9e..430defc2e3e319 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -181,7 +181,7 @@ aioairzone-cloud==0.7.2 aioairzone==1.0.4 # homeassistant.components.alexa_devices -aioamazondevices==10.0.0 +aioamazondevices==11.0.2 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -820,7 +820,7 @@ esphome-dashboard-api==1.3.0 essent-dynamic-pricing==0.2.7 # homeassistant.components.netgear_lte -eternalegypt==0.0.16 +eternalegypt==0.0.18 # homeassistant.components.eufylife_ble eufylife-ble-client==0.1.8 @@ -2414,7 +2414,7 @@ smart-meter-texas==0.5.5 snapcast==2.3.6 # homeassistant.components.sonos -soco==0.30.13 +soco==0.30.14 # homeassistant.components.solaredge solaredge-web==0.0.1 diff --git a/tests/components/hikvision/test_binary_sensor.py b/tests/components/hikvision/test_binary_sensor.py index 5eff8508957ccc..7c659c1c11a766 100644 --- a/tests/components/hikvision/test_binary_sensor.py +++ b/tests/components/hikvision/test_binary_sensor.py @@ -294,6 +294,10 @@ async def test_binary_sensor_update_callback( callback_func = add_callback_call[0][0] callback_func("motion detected") + # Wait for the event loop to process the scheduled state update + # (callback uses call_soon_threadsafe to schedule update in event loop) + await hass.async_block_till_done() + # Verify state was updated state = hass.states.get("binary_sensor.front_camera_motion") assert state is not None diff --git a/tests/components/hydrawise/snapshots/test_sensor.ambr b/tests/components/hydrawise/snapshots/test_sensor.ambr index e2e97da120c273..9a552db39840c0 100644 --- a/tests/components/hydrawise/snapshots/test_sensor.ambr +++ b/tests/components/hydrawise/snapshots/test_sensor.ambr @@ -28,7 +28,7 @@ 'suggested_unit_of_measurement': , }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': 'Daily active water use', 'platform': 'hydrawise', @@ -44,7 +44,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by hydrawise.com', - 'device_class': 'volume', + 'device_class': 'water', 'friendly_name': 'Home Controller Daily active water use', 'unit_of_measurement': , }), @@ -139,7 +139,7 @@ 'suggested_unit_of_measurement': , }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': 'Daily inactive water use', 'platform': 'hydrawise', @@ -155,7 +155,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by hydrawise.com', - 'device_class': 'volume', + 'device_class': 'water', 'friendly_name': 'Home Controller Daily inactive water use', 'unit_of_measurement': , }), @@ -196,7 +196,7 @@ 'suggested_unit_of_measurement': , }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': 'Daily total water use', 'platform': 'hydrawise', @@ -212,7 +212,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by hydrawise.com', - 'device_class': 'volume', + 'device_class': 'water', 'friendly_name': 'Home Controller Daily total water use', 'unit_of_measurement': , }), @@ -253,7 +253,7 @@ 'suggested_unit_of_measurement': , }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, 'original_name': 'Daily active water use', 'platform': 'hydrawise', @@ -269,7 +269,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by hydrawise.com', - 'device_class': 'volume', + 'device_class': 'water', 'friendly_name': 'Zone One Daily active water use', 'unit_of_measurement': , }), @@ -464,7 +464,7 @@ 'suggested_unit_of_measurement': , }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': 'mdi:water-outline', 'original_name': 'Daily active water use', 'platform': 'hydrawise', @@ -480,7 +480,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by hydrawise.com', - 'device_class': 'volume', + 'device_class': 'water', 'friendly_name': 'Zone Two Daily active water use', 'icon': 'mdi:water-outline', 'unit_of_measurement': , diff --git a/tests/components/netgear_lte/test_config_flow.py b/tests/components/netgear_lte/test_config_flow.py index ec649f4def03ea..9e39ec54673bfe 100644 --- a/tests/components/netgear_lte/test_config_flow.py +++ b/tests/components/netgear_lte/test_config_flow.py @@ -65,18 +65,3 @@ async def test_flow_user_cannot_connect( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" - - -async def test_flow_user_unknown_error(hass: HomeAssistant, unknown: None) -> None: - """Test unknown error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_USER}, - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=CONF_DATA, - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"]["base"] == "unknown" diff --git a/tests/components/netgear_lte/test_services.py b/tests/components/netgear_lte/test_services.py index 58e57cf2039bb2..a7ca18fd4d2b0e 100644 --- a/tests/components/netgear_lte/test_services.py +++ b/tests/components/netgear_lte/test_services.py @@ -2,9 +2,12 @@ from unittest.mock import patch +import pytest + from homeassistant.components.netgear_lte.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from .conftest import HOST @@ -54,3 +57,15 @@ async def test_set_option(hass: HomeAssistant, setup_integration: None) -> None: blocking=True, ) assert len(mock_client.mock_calls) == 1 + + entry = hass.config_entries.async_entries(DOMAIN)[0] + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + DOMAIN, + "delete_sms", + {CONF_HOST: "no-match", "sms_id": 1}, + blocking=True, + )