diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index 4ead41e86e9..1d1c26b5fcd 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/apple_tv", "iot_class": "local_push", "loggers": ["pyatv", "srptools"], - "requirements": ["pyatv==0.13.2"], + "requirements": ["pyatv==0.13.3"], "zeroconf": [ "_mediaremotetv._tcp.local.", "_companion-link._tcp.local.", diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index bc07e2b94ae..67c27f014d1 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -19,6 +19,6 @@ "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", "bluetooth-data-tools==1.6.1", - "dbus-fast==1.87.5" + "dbus-fast==1.90.1" ] } diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 1b865827e69..dd5ad2d5190 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -15,9 +15,11 @@ from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, STATE_CLASSES_SCHEMA, + SensorDeviceClass, SensorEntity, SensorStateClass, ) +from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( CONF_COMMAND, CONF_DEVICE_CLASS, @@ -206,14 +208,24 @@ class CommandSensor(ManualTriggerEntity, SensorEntity): return if self._value_template is not None: - self._attr_native_value = ( - self._value_template.async_render_with_possible_json_value( - value, - None, - ) + value = self._value_template.async_render_with_possible_json_value( + value, + None, ) - else: + + if self.device_class not in { + SensorDeviceClass.DATE, + SensorDeviceClass.TIMESTAMP, + }: self._attr_native_value = value + self._process_manual_data(value) + return + + self._attr_native_value = None + if value is not None: + self._attr_native_value = async_parse_date_datetime( + value, self.entity_id, self.device_class + ) self._process_manual_data(value) self.async_write_ha_state() diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 1eb58e96ff9..9e0909b6dfc 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.2.5", "home-assistant-intents==2023.7.25"] + "requirements": ["hassil==1.2.5", "home-assistant-intents==2023.8.2"] } diff --git a/homeassistant/components/duotecno/manifest.json b/homeassistant/components/duotecno/manifest.json index ae82574146e..69490b6b5aa 100644 --- a/homeassistant/components/duotecno/manifest.json +++ b/homeassistant/components/duotecno/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/duotecno", "iot_class": "local_push", - "requirements": ["pyduotecno==2023.8.0"] + "requirements": ["pyduotecno==2023.8.3"] } diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 1ba93da716c..a98d2c08a48 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -6,6 +6,7 @@ import logging from aiohttp import web import voluptuous as vol +from homeassistant.components.http import HomeAssistantAccessLogger from homeassistant.components.network import async_get_source_ip from homeassistant.const import ( CONF_ENTITIES, @@ -100,7 +101,7 @@ async def start_emulated_hue_bridge( config.advertise_port or config.listen_port, ) - runner = web.AppRunner(app) + runner = web.AppRunner(app, access_log_class=HomeAssistantAccessLogger) await runner.setup() site = web.TCPSite(runner, config.host_ip_addr, config.listen_port) diff --git a/homeassistant/components/forked_daapd/__init__.py b/homeassistant/components/forked_daapd/__init__.py index 14f40db2057..9dfb92c60c8 100644 --- a/homeassistant/components/forked_daapd/__init__.py +++ b/homeassistant/components/forked_daapd/__init__.py @@ -18,9 +18,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Remove forked-daapd component.""" status = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if status and hass.data.get(DOMAIN) and hass.data[DOMAIN].get(entry.entry_id): - hass.data[DOMAIN][entry.entry_id][ + if websocket_handler := hass.data[DOMAIN][entry.entry_id][ HASS_DATA_UPDATER_KEY - ].websocket_handler.cancel() + ].websocket_handler: + websocket_handler.cancel() for remove_listener in hass.data[DOMAIN][entry.entry_id][ HASS_DATA_REMOVE_LISTENERS_KEY ]: diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index e1f1ece055b..868ec8e1f9e 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -31,6 +31,7 @@ from homeassistant.components.spotify import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -127,10 +128,10 @@ async def async_setup_entry( forked_daapd_updater = ForkedDaapdUpdater( hass, forked_daapd_api, config_entry.entry_id ) - await forked_daapd_updater.async_init() hass.data[DOMAIN][config_entry.entry_id][ HASS_DATA_UPDATER_KEY ] = forked_daapd_updater + await forked_daapd_updater.async_init() async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: @@ -914,7 +915,8 @@ class ForkedDaapdUpdater: async def async_init(self): """Perform async portion of class initialization.""" - server_config = await self._api.get_request("config") + if not (server_config := await self._api.get_request("config")): + raise PlatformNotReady if websocket_port := server_config.get("websocket_port"): self.websocket_handler = asyncio.create_task( self._api.start_websocket_handler( diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 4a9c22847ae..122242f1959 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -161,10 +161,13 @@ class FreeboxRouter: async def _update_raids_sensors(self) -> None: """Update Freebox raids.""" # None at first request - fbx_raids: list[dict[str, Any]] = await self._api.storage.get_raids() or [] - - for fbx_raid in fbx_raids: - self.raids[fbx_raid["id"]] = fbx_raid + try: + fbx_raids: list[dict[str, Any]] = await self._api.storage.get_raids() or [] + except HttpRequestError: + _LOGGER.warning("Unable to enumerate raid disks") + else: + for fbx_raid in fbx_raids: + self.raids[fbx_raid["id"]] = fbx_raid async def update_home_devices(self) -> None: """Update Home devices (alarm, light, sensor, switch, remote ...).""" diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index cdea8ebee54..8dfe5be9308 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -469,6 +469,11 @@ class FritzBoxTools( if not host.get("MACAddress"): continue + if (wan_access := host.get("X_AVM-DE_WANAccess")) is not None: + wan_access_result = "granted" in wan_access + else: + wan_access_result = None + hosts[host["MACAddress"]] = Device( name=host["HostName"], connected=host["Active"], @@ -476,7 +481,7 @@ class FritzBoxTools( connection_type="", ip_address=host["IPAddress"], ssid=None, - wan_access="granted" in host["X_AVM-DE_WANAccess"], + wan_access=wan_access_result, ) if not self.fritz_status.device_has_mesh_support or ( diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index 957aa4a7806..f42da406599 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -89,6 +89,7 @@ class HueLight(HueBaseEntity, LightEntity): self._supported_color_modes.add(ColorMode.BRIGHTNESS) # support transition if brightness control self._attr_supported_features |= LightEntityFeature.TRANSITION + self._color_temp_active: bool = False # get list of supported effects (combine effects and timed_effects) self._attr_effect_list = [] if effects := resource.effects: @@ -121,10 +122,8 @@ class HueLight(HueBaseEntity, LightEntity): @property def color_mode(self) -> ColorMode: """Return the color mode of the light.""" - if color_temp := self.resource.color_temperature: - # Hue lights return `mired_valid` to indicate CT is active - if color_temp.mirek is not None: - return ColorMode.COLOR_TEMP + if self.color_temp_active: + return ColorMode.COLOR_TEMP if self.resource.supports_color: return ColorMode.XY if self.resource.supports_dimming: @@ -132,6 +131,18 @@ class HueLight(HueBaseEntity, LightEntity): # fallback to on_off return ColorMode.ONOFF + @property + def color_temp_active(self) -> bool: + """Return if the light is in Color Temperature mode.""" + color_temp = self.resource.color_temperature + if color_temp is None or color_temp.mirek is None: + return False + # Official Hue lights return `mirek_valid` to indicate CT is active + # while non-official lights do not. + if self.device.product_data.certified: + return self.resource.color_temperature.mirek_valid + return self._color_temp_active + @property def xy_color(self) -> tuple[float, float] | None: """Return the xy color.""" @@ -193,6 +204,7 @@ class HueLight(HueBaseEntity, LightEntity): xy_color = kwargs.get(ATTR_XY_COLOR) color_temp = normalize_hue_colortemp(kwargs.get(ATTR_COLOR_TEMP)) brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS)) + self._color_temp_active = color_temp is not None flash = kwargs.get(ATTR_FLASH) effect = effect_str = kwargs.get(ATTR_EFFECT) if effect_str in (EFFECT_NONE, EFFECT_NONE.lower()): diff --git a/homeassistant/components/keymitt_ble/config_flow.py b/homeassistant/components/keymitt_ble/config_flow.py index 49ab04163dc..e8176b152a6 100644 --- a/homeassistant/components/keymitt_ble/config_flow.py +++ b/homeassistant/components/keymitt_ble/config_flow.py @@ -138,7 +138,7 @@ class MicroBotConfigFlow(ConfigFlow, domain=DOMAIN): await self._client.connect(init=True) return self.async_show_form(step_id="link") - if not self._client.is_connected(): + if not await self._client.is_connected(): errors["base"] = "linking" else: await self._client.disconnect() diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 1bb6d9bbdd2..3444e9b002a 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -342,10 +342,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms( entry, [ - platform - for platform in SUPPORTED_PLATFORMS - if platform in hass.data[DATA_KNX_CONFIG] - and platform is not Platform.NOTIFY + Platform.SENSOR, # always unload system entities (telegram counter, etc.) + *[ + platform + for platform in SUPPORTED_PLATFORMS + if platform in hass.data[DATA_KNX_CONFIG] + and platform not in (Platform.SENSOR, Platform.NOTIFY) + ], ], ) if unload_ok: diff --git a/homeassistant/components/kostal_plenticore/select.py b/homeassistant/components/kostal_plenticore/select.py index 2118d4b47c6..1a89e5617cc 100644 --- a/homeassistant/components/kostal_plenticore/select.py +++ b/homeassistant/components/kostal_plenticore/select.py @@ -111,7 +111,7 @@ class PlenticoreDataSelect( self.platform_name = platform_name self.module_id = description.module_id self.data_id = description.key - self._device_info = device_info + self._attr_device_info = device_info self._attr_unique_id = f"{entry_id}_{description.module_id}" @property diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index 109af7a565b..e5718d5132f 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -26,7 +26,6 @@ CONF_STATION = "station" ATTRIBUTION = "Data from National Weather Service/NOAA" ATTR_FORECAST_DETAILED_DESCRIPTION = "detailed_description" -ATTR_FORECAST_DAYTIME = "daytime" CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_EXCEPTIONAL: [ diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index e8a35ba66f1..8ddf842cd62 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -9,6 +9,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, ATTR_FORECAST_HUMIDITY, + ATTR_FORECAST_IS_DAYTIME, ATTR_FORECAST_NATIVE_DEW_POINT, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_WIND_SPEED, @@ -36,7 +37,6 @@ from homeassistant.util.unit_system import UnitSystem from . import base_unique_id, device_info from .const import ( - ATTR_FORECAST_DAYTIME, ATTR_FORECAST_DETAILED_DESCRIPTION, ATTRIBUTION, CONDITION_CLASSES, @@ -101,7 +101,6 @@ if TYPE_CHECKING: """Forecast with extra fields needed for NWS.""" detailed_description: str | None - daytime: bool | None class NWSWeather(WeatherEntity): @@ -268,7 +267,7 @@ class NWSWeather(WeatherEntity): data[ATTR_FORECAST_HUMIDITY] = forecast_entry.get("relativeHumidity") if self.mode == DAYNIGHT: - data[ATTR_FORECAST_DAYTIME] = forecast_entry.get("isDaytime") + data[ATTR_FORECAST_IS_DAYTIME] = forecast_entry.get("isDaytime") time = forecast_entry.get("iconTime") weather = forecast_entry.get("iconWeather") diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index c0eb319c10c..94758720722 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["recorder"], "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", - "requirements": ["opower==0.0.18"] + "requirements": ["opower==0.0.20"] } diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index c841e3b0e36..b5296d772df 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -67,7 +67,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:battery", device_class=SensorDeviceClass.ENUM, - options=["full", "normal", "low", "verylow"], + options=["full", "normal", "medium", "low", "verylow"], translation_key="battery", ), OverkizSensorDescription( diff --git a/homeassistant/components/overkiz/strings.json b/homeassistant/components/overkiz/strings.json index c4daf32499a..bcf1e121f6f 100644 --- a/homeassistant/components/overkiz/strings.json +++ b/homeassistant/components/overkiz/strings.json @@ -77,6 +77,7 @@ "full": "Full", "low": "Low", "normal": "Normal", + "medium": "Medium", "verylow": "Very low" } }, diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index d26116a7818..eda6a5609a2 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.30.2"] + "requirements": ["python-roborock==0.31.1"] } diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index a69d2a4c382..936dc998c86 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -218,6 +218,8 @@ async def async_setup_entry( class SolarlogSensor(CoordinatorEntity[SolarlogData], SensorEntity): """Representation of a Sensor.""" + _attr_has_entity_name = True + entity_description: SolarLogSensorEntityDescription def __init__( @@ -228,7 +230,6 @@ class SolarlogSensor(CoordinatorEntity[SolarlogData], SensorEntity): """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{coordinator.name} {description.name}" self._attr_unique_id = f"{coordinator.unique_id}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, coordinator.unique_id)}, diff --git a/homeassistant/components/thread/discovery.py b/homeassistant/components/thread/discovery.py index 1006a44d5d3..d07469f36fb 100644 --- a/homeassistant/components/thread/discovery.py +++ b/homeassistant/components/thread/discovery.py @@ -57,25 +57,29 @@ def async_discovery_data_from_service( except UnicodeDecodeError: return None - ext_addr = service.properties.get(b"xa") - ext_pan_id = service.properties.get(b"xp") - network_name = try_decode(service.properties.get(b"nn")) - model_name = try_decode(service.properties.get(b"mn")) + # Service properties are always bytes if they are set from the network. + # For legacy backwards compatibility zeroconf allows properties to be set + # as strings but we never do that so we can safely cast here. + service_properties = cast(dict[bytes, bytes | None], service.properties) + ext_addr = service_properties.get(b"xa") + ext_pan_id = service_properties.get(b"xp") + network_name = try_decode(service_properties.get(b"nn")) + model_name = try_decode(service_properties.get(b"mn")) server = service.server - vendor_name = try_decode(service.properties.get(b"vn")) - thread_version = try_decode(service.properties.get(b"tv")) + vendor_name = try_decode(service_properties.get(b"vn")) + thread_version = try_decode(service_properties.get(b"tv")) unconfigured = None brand = KNOWN_BRANDS.get(vendor_name) if brand == "homeassistant": # Attempt to detect incomplete configuration - if (state_bitmap_b := service.properties.get(b"sb")) is not None: + if (state_bitmap_b := service_properties.get(b"sb")) is not None: try: state_bitmap = StateBitmap.from_bytes(state_bitmap_b) if not state_bitmap.is_active: unconfigured = True except ValueError: _LOGGER.debug("Failed to decode state bitmap in service %s", service) - if service.properties.get(b"at") is None: + if service_properties.get(b"at") is None: unconfigured = True return ThreadRouterDiscoveryData( @@ -168,10 +172,19 @@ class ThreadRouterDiscovery: return _LOGGER.debug("_add_update_service %s %s", name, service) + # Service properties are always bytes if they are set from the network. + # For legacy backwards compatibility zeroconf allows properties to be set + # as strings but we never do that so we can safely cast here. + service_properties = cast(dict[bytes, bytes | None], service.properties) + + if not (xa := service_properties.get(b"xa")): + _LOGGER.debug("_add_update_service failed to find xa in %s", service) + return + # We use the extended mac address as key, bail out if it's missing try: - extended_mac_address = service.properties[b"xa"].hex() - except (KeyError, UnicodeDecodeError) as err: + extended_mac_address = xa.hex() + except UnicodeDecodeError as err: _LOGGER.debug("_add_update_service failed to parse service %s", err) return diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index c33106d13cc..b2fcc5c0161 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -154,7 +154,7 @@ }, { "hostname": "k[lps]*", - "macaddress": "788C5B*" + "macaddress": "788CB5*" } ], "documentation": "https://www.home-assistant.io/integrations/tplink", diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 6c843246663..fb812abc293 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -116,7 +116,7 @@ class SmartPlugSwitchChild(SmartPlugSwitch): coordinator: TPLinkDataUpdateCoordinator, plug: SmartDevice, ) -> None: - """Initialize the switch.""" + """Initialize the child switch.""" super().__init__(device, coordinator) self._plug = plug self._attr_unique_id = legacy_device_id(plug) @@ -124,10 +124,15 @@ class SmartPlugSwitchChild(SmartPlugSwitch): @async_refresh_after async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the switch on.""" + """Turn the child switch on.""" await self._plug.turn_on() @async_refresh_after async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the switch off.""" + """Turn the child switch off.""" await self._plug.turn_off() + + @property + def is_on(self) -> bool: + """Return true if child switch is on.""" + return bool(self._plug.is_on) diff --git a/homeassistant/components/unifi/image.py b/homeassistant/components/unifi/image.py index dc4fb93eded..c3969c21bc4 100644 --- a/homeassistant/components/unifi/image.py +++ b/homeassistant/components/unifi/image.py @@ -41,7 +41,7 @@ class UnifiImageEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): """Validate and load entities from different UniFi handlers.""" image_fn: Callable[[UniFiController, ApiItemT], bytes] - value_fn: Callable[[ApiItemT], str] + value_fn: Callable[[ApiItemT], str | None] @dataclass @@ -99,7 +99,7 @@ class UnifiImageEntity(UnifiEntity[HandlerT, ApiItemT], ImageEntity): _attr_content_type = "image/png" current_image: bytes | None = None - previous_value = "" + previous_value: str | None = None def __init__( self, diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index c34d1035158..4cc45ddb6b8 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==50"], + "requirements": ["aiounifi==51"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index a3bf027c28f..e5347b204e6 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -86,10 +86,10 @@ def _setup_entities(devices, async_add_entities): class VeSyncFanHA(VeSyncDevice, FanEntity): """Representation of a VeSync fan.""" - _attr_supported_features = FanEntityFeature.SET_SPEED + _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE _attr_name = None - def __init__(self, fan): + def __init__(self, fan) -> None: """Initialize the VeSync fan device.""" super().__init__(fan) self.smartfan = fan diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index e91e3da5aa5..71ec703df3f 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from contextlib import suppress from datetime import timedelta import logging @@ -141,8 +142,9 @@ class WaqiSensor(SensorEntity): @property def native_value(self): """Return the state of the device.""" - if self._data is not None: - return self._data.get("aqi") + if (value := self._data.get("aqi")) is not None: + with suppress(ValueError): + return float(value) return None @property diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index f77909b1bdd..b85f9f0fd83 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -553,11 +553,17 @@ def info_from_service(service: AsyncServiceInfo) -> ZeroconfServiceInfo | None: break if not host: return None + + # Service properties are always bytes if they are set from the network. + # For legacy backwards compatibility zeroconf allows properties to be set + # as strings but we never do that so we can safely cast here. + service_properties = cast(dict[bytes, bytes | None], service.properties) + properties: dict[str, Any] = { k.decode("ascii", "replace"): None if v is None else v.decode("utf-8", "replace") - for k, v in service.properties.items() + for k, v in service_properties.items() } assert service.server is not None, "server cannot be none if there are addresses" diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 73ebe15d0c7..cd7b9e95e75 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.72.0"] + "requirements": ["zeroconf==0.74.0"] } diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 9e71691aaa5..73955614c07 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -329,7 +329,7 @@ class BaseLight(LogMixin, light.LightEntity): return if ( - (brightness is not None or transition) + (brightness is not None or transition is not None) and not new_color_provided_while_off and brightness_supported(self._attr_supported_color_modes) ): @@ -350,11 +350,11 @@ class BaseLight(LogMixin, light.LightEntity): self._attr_brightness = level if ( - brightness is None + (brightness is None and transition is None) and not new_color_provided_while_off - or (self._FORCE_ON and brightness) + or (self._FORCE_ON and brightness != 0) ): - # since some lights don't always turn on with move_to_level_with_on_off, + # since FORCE_ON lights don't turn on with move_to_level_with_on_off, # we should call the on command on the on_off cluster # if brightness is not 0. result = await self._on_off_cluster_handler.on() @@ -385,7 +385,7 @@ class BaseLight(LogMixin, light.LightEntity): return if new_color_provided_while_off: - # The light is has the correct color, so we can now transition + # The light has the correct color, so we can now transition # it to the correct brightness level. result = await self._level_cluster_handler.move_to_level( level=level, transition_time=int(10 * duration) @@ -1076,7 +1076,7 @@ class HueLight(Light): manufacturers={"Jasco", "Quotra-Vision", "eWeLight", "eWeLink"}, ) class ForceOnLight(Light): - """Representation of a light which does not respect move_to_level_with_on_off.""" + """Representation of a light which does not respect on/off for move_to_level_with_on_off commands.""" _attr_name: str = "Light" _FORCE_ON = True diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 5e33377ec0e..041a93a8ead 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -25,10 +25,10 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.102", "zigpy-deconz==0.21.0", - "zigpy==0.56.2", + "zigpy==0.56.4", "zigpy-xbee==0.18.1", "zigpy-zigate==0.11.0", - "zigpy-znp==0.11.3" + "zigpy-znp==0.11.4" ], "usb": [ { diff --git a/homeassistant/const.py b/homeassistant/const.py index db7ef9e305a..0568650deb5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 8b5dd91f64c..91a02ac3e06 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -782,7 +782,7 @@ DHCP: list[dict[str, str | bool]] = [ { "domain": "tplink", "hostname": "k[lps]*", - "macaddress": "788C5B*", + "macaddress": "788CB5*", }, { "domain": "tuya", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7401c747890..fa5b52478d0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,15 +15,15 @@ bluetooth-auto-recovery==1.2.1 bluetooth-data-tools==1.6.1 certifi>=2021.5.30 ciso8601==2.3.0 -cryptography==41.0.2 -dbus-fast==1.87.5 +cryptography==41.0.3 +dbus-fast==1.90.1 fnv-hash-fast==0.4.0 ha-av==10.1.1 hass-nabucasa==0.69.0 hassil==1.2.5 home-assistant-bluetooth==1.10.2 home-assistant-frontend==20230802.0 -home-assistant-intents==2023.7.25 +home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 janus==1.0.0 @@ -52,7 +52,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.72.0 +zeroconf==0.74.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/pyproject.toml b/pyproject.toml index 26a9525a176..fdcd2788b58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.8.0" +version = "2023.8.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" @@ -41,7 +41,7 @@ dependencies = [ "lru-dict==1.2.0", "PyJWT==2.8.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==41.0.2", + "cryptography==41.0.3", # pyOpenSSL 23.2.0 is required to work with cryptography 41+ "pyOpenSSL==23.2.0", "orjson==3.9.2", diff --git a/requirements.txt b/requirements.txt index 9f5023c9a1c..4106a8515d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ ifaddr==0.2.0 Jinja2==3.1.2 lru-dict==1.2.0 PyJWT==2.8.0 -cryptography==41.0.2 +cryptography==41.0.3 pyOpenSSL==23.2.0 orjson==3.9.2 pip>=21.3.1 diff --git a/requirements_all.txt b/requirements_all.txt index 28140477411..21d10628ecb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -360,7 +360,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==50 +aiounifi==51 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -632,7 +632,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.87.5 +dbus-fast==1.90.1 # homeassistant.components.debugpy debugpy==1.6.7 @@ -991,7 +991,7 @@ holidays==0.28 home-assistant-frontend==20230802.0 # homeassistant.components.conversation -home-assistant-intents==2023.7.25 +home-assistant-intents==2023.8.2 # homeassistant.components.home_connect homeconnect==0.7.2 @@ -1368,7 +1368,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.18 +opower==0.0.20 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -1572,7 +1572,7 @@ pyatag==0.3.5.3 pyatmo==7.5.0 # homeassistant.components.apple_tv -pyatv==0.13.2 +pyatv==0.13.3 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 @@ -1650,7 +1650,7 @@ pydrawise==2023.7.1 pydroid-ipcam==2.0.0 # homeassistant.components.duotecno -pyduotecno==2023.8.0 +pyduotecno==2023.8.3 # homeassistant.components.ebox pyebox==1.1.4 @@ -2150,7 +2150,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.30.2 +python-roborock==0.31.1 # homeassistant.components.smarttub python-smarttub==0.0.33 @@ -2749,7 +2749,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.72.0 +zeroconf==0.74.0 # homeassistant.components.zeversolar zeversolar==0.3.1 @@ -2773,10 +2773,10 @@ zigpy-xbee==0.18.1 zigpy-zigate==0.11.0 # homeassistant.components.zha -zigpy-znp==0.11.3 +zigpy-znp==0.11.4 # homeassistant.components.zha -zigpy==0.56.2 +zigpy==0.56.4 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03dc3bbf994..5cb970c7132 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==50 +aiounifi==51 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -515,7 +515,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.87.5 +dbus-fast==1.90.1 # homeassistant.components.debugpy debugpy==1.6.7 @@ -777,7 +777,7 @@ holidays==0.28 home-assistant-frontend==20230802.0 # homeassistant.components.conversation -home-assistant-intents==2023.7.25 +home-assistant-intents==2023.8.2 # homeassistant.components.home_connect homeconnect==0.7.2 @@ -1037,7 +1037,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.18 +opower==0.0.20 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -1178,7 +1178,7 @@ pyatag==0.3.5.3 pyatmo==7.5.0 # homeassistant.components.apple_tv -pyatv==0.13.2 +pyatv==0.13.3 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 @@ -1223,7 +1223,7 @@ pydiscovergy==2.0.1 pydroid-ipcam==2.0.0 # homeassistant.components.duotecno -pyduotecno==2023.8.0 +pyduotecno==2023.8.3 # homeassistant.components.econet pyeconet==0.1.20 @@ -1579,7 +1579,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.30.2 +python-roborock==0.31.1 # homeassistant.components.smarttub python-smarttub==0.0.33 @@ -2022,7 +2022,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.72.0 +zeroconf==0.74.0 # homeassistant.components.zeversolar zeversolar==0.3.1 @@ -2040,10 +2040,10 @@ zigpy-xbee==0.18.1 zigpy-zigate==0.11.0 # homeassistant.components.zha -zigpy-znp==0.11.3 +zigpy-znp==0.11.4 # homeassistant.components.zha -zigpy==0.56.2 +zigpy==0.56.4 # homeassistant.components.zwave_js zwave-js-server-python==0.49.0 diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 1754c166ef7..22c3e927703 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -61,8 +61,9 @@ def allow_name_translation(integration: Integration) -> bool: """Validate that the translation name is not the same as the integration name.""" # Only enforce for core because custom integrations can't be # added to allow list. - return integration.core and ( - integration.domain in ALLOW_NAME_TRANSLATION + return ( + not integration.core + or integration.domain in ALLOW_NAME_TRANSLATION or integration.quality_scale == "internal" ) diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index a0f8f2cdf84..bc24ff5419f 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -646,3 +646,54 @@ async def test_updating_manually( ) await hass.async_block_till_done() assert called + + +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "echo 2022-12-22T13:15:30Z", + "device_class": "timestamp", + } + } + ] + } + ], +) +async def test_scrape_sensor_device_timestamp( + hass: HomeAssistant, load_yaml_integration: None +) -> None: + """Test Command Line sensor with a device of type TIMESTAMP.""" + entity_state = hass.states.get("sensor.test") + assert entity_state + assert entity_state.state == "2022-12-22T13:15:30+00:00" + + +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "echo January 17, 2022", + "device_class": "date", + "value_template": "{{ strptime(value, '%B %d, %Y').strftime('%Y-%m-%d') }}", + } + } + ] + } + ], +) +async def test_scrape_sensor_device_date( + hass: HomeAssistant, load_yaml_integration: None +) -> None: + """Test Command Line sensor with a device of type DATE.""" + entity_state = hass.states.get("sensor.test") + assert entity_state + assert entity_state.state == "2022-01-17" diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index f9fe284bcb0..f7145a9ab56 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -30,6 +30,7 @@ 'id': 'homeassistant', 'name': 'Home Assistant', 'supported_languages': list([ + 'af', 'ar', 'bg', 'bn', diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index 81357b6f3eb..fc02cdb4123 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -1,5 +1,5 @@ """The config flow tests for the forked_daapd media player platform.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -12,9 +12,11 @@ from homeassistant.components.forked_daapd.const import ( CONF_TTS_VOLUME, DOMAIN, ) +from homeassistant.components.forked_daapd.media_player import async_setup_entry from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from tests.common import MockConfigEntry @@ -242,3 +244,18 @@ async def test_options_flow(hass: HomeAssistant, config_entry) -> None: }, ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + + +async def test_async_setup_entry_not_ready(hass: HomeAssistant, config_entry) -> None: + """Test that a PlatformNotReady exception is thrown during platform setup.""" + + with patch( + "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI", + autospec=True, + ) as mock_api: + mock_api.return_value.get_request.return_value = None + config_entry.add_to_hass(hass) + with pytest.raises(PlatformNotReady): + await async_setup_entry(hass, config_entry, MagicMock()) + await hass.async_block_till_done() + mock_api.return_value.get_request.assert_called_once() diff --git a/tests/components/keymitt_ble/__init__.py b/tests/components/keymitt_ble/__init__.py index 2938e22c924..c6e56739d76 100644 --- a/tests/components/keymitt_ble/__init__.py +++ b/tests/components/keymitt_ble/__init__.py @@ -77,6 +77,6 @@ class MockMicroBotApiClientFail: async def disconnect(self): """Mock disconnect.""" - def is_connected(self): + async def is_connected(self): """Mock disconnected.""" return False diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 4232d3e6909..816251ae3bb 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -180,12 +180,14 @@ def _mocked_strip() -> SmartStrip: plug0.alias = "Plug0" plug0.device_id = "bb:bb:cc:dd:ee:ff_PLUG0DEVICEID" plug0.mac = "bb:bb:cc:dd:ee:ff" + plug0.is_on = True plug0.protocol = _mock_protocol() plug1 = _mocked_plug() plug1.device_id = "cc:bb:cc:dd:ee:ff_PLUG1DEVICEID" plug1.mac = "cc:bb:cc:dd:ee:ff" plug1.alias = "Plug1" plug1.protocol = _mock_protocol() + plug1.is_on = False strip.children = [plug0, plug1] return strip diff --git a/tests/components/tplink/test_switch.py b/tests/components/tplink/test_switch.py index 1e5e03c0f37..05286e5ff48 100644 --- a/tests/components/tplink/test_switch.py +++ b/tests/components/tplink/test_switch.py @@ -9,7 +9,7 @@ import pytest from homeassistant.components import tplink from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.tplink.const import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -146,22 +146,37 @@ async def test_strip(hass: HomeAssistant) -> None: # since this is what the previous version did assert hass.states.get("switch.my_strip") is None - for plug_id in range(2): - entity_id = f"switch.my_strip_plug{plug_id}" - state = hass.states.get(entity_id) - assert state.state == STATE_ON + entity_id = "switch.my_strip_plug0" + state = hass.states.get(entity_id) + assert state.state == STATE_ON - await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True - ) - strip.children[plug_id].turn_off.assert_called_once() - strip.children[plug_id].turn_off.reset_mock() + await hass.services.async_call( + SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + strip.children[0].turn_off.assert_called_once() + strip.children[0].turn_off.reset_mock() - await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True - ) - strip.children[plug_id].turn_on.assert_called_once() - strip.children[plug_id].turn_on.reset_mock() + await hass.services.async_call( + SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + strip.children[0].turn_on.assert_called_once() + strip.children[0].turn_on.reset_mock() + + entity_id = "switch.my_strip_plug1" + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + strip.children[1].turn_off.assert_called_once() + strip.children[1].turn_off.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + strip.children[1].turn_on.assert_called_once() + strip.children[1].turn_on.reset_mock() async def test_strip_unique_ids(hass: HomeAssistant) -> None: diff --git a/tests/components/vesync/snapshots/test_diagnostics.ambr b/tests/components/vesync/snapshots/test_diagnostics.ambr index c463db179eb..10dfdd2ba14 100644 --- a/tests/components/vesync/snapshots/test_diagnostics.ambr +++ b/tests/components/vesync/snapshots/test_diagnostics.ambr @@ -200,7 +200,7 @@ 'auto', 'sleep', ]), - 'supported_features': 1, + 'supported_features': 9, }), 'entity_id': 'fan.fan', 'last_changed': str, diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index 428f066e6cc..fa1a7a7b332 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -58,7 +58,7 @@ 'original_icon': None, 'original_name': None, 'platform': 'vesync', - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': 'air-purifier', 'unit_of_measurement': None, @@ -73,7 +73,7 @@ 'auto', 'sleep', ]), - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.air_purifier_131s', @@ -140,7 +140,7 @@ 'original_icon': None, 'original_name': None, 'platform': 'vesync', - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': 'asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55', 'unit_of_measurement': None, @@ -161,7 +161,7 @@ 'sleep', ]), 'screen_status': True, - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.air_purifier_200s', @@ -229,7 +229,7 @@ 'original_icon': None, 'original_name': None, 'platform': 'vesync', - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '400s-purifier', 'unit_of_measurement': None, @@ -251,7 +251,7 @@ 'sleep', ]), 'screen_status': True, - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.air_purifier_400s', @@ -319,7 +319,7 @@ 'original_icon': None, 'original_name': None, 'platform': 'vesync', - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '600s-purifier', 'unit_of_measurement': None, @@ -341,7 +341,7 @@ 'sleep', ]), 'screen_status': True, - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.air_purifier_600s', diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 3abfd0e4f9c..c1f5cf04e35 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -530,7 +530,78 @@ async def test_transitions( light2_state = hass.states.get(device_2_entity_id) assert light2_state.state == STATE_OFF - # first test 0 length transition with no color provided + # first test 0 length transition with no color and no brightness provided + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_level.request.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {"entity_id": device_1_entity_id, "transition": 0}, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 1 + assert dev1_cluster_level.request.await_count == 1 + assert dev1_cluster_level.request.call_args == call( + False, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].id, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + level=254, # default "full on" brightness + transition_time=0, + expect_reply=True, + manufacturer=None, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 254 + + # test 0 length transition with no color and no brightness provided again, but for "force on" lights + eWeLink_cluster_on_off.request.reset_mock() + eWeLink_cluster_level.request.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {"entity_id": eWeLink_light_entity_id, "transition": 0}, + blocking=True, + ) + assert eWeLink_cluster_on_off.request.call_count == 1 + assert eWeLink_cluster_on_off.request.await_count == 1 + assert eWeLink_cluster_on_off.request.call_args_list[0] == call( + False, + eWeLink_cluster_on_off.commands_by_name["on"].id, + eWeLink_cluster_on_off.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tsn=None, + ) + assert eWeLink_cluster_color.request.call_count == 0 + assert eWeLink_cluster_color.request.await_count == 0 + assert eWeLink_cluster_level.request.call_count == 1 + assert eWeLink_cluster_level.request.await_count == 1 + assert eWeLink_cluster_level.request.call_args == call( + False, + eWeLink_cluster_level.commands_by_name["move_to_level_with_on_off"].id, + eWeLink_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + level=254, # default "full on" brightness + transition_time=0, + expect_reply=True, + manufacturer=None, + tsn=None, + ) + + eWeLink_state = hass.states.get(eWeLink_light_entity_id) + assert eWeLink_state.state == STATE_ON + assert eWeLink_state.attributes["brightness"] == 254 + + eWeLink_cluster_on_off.request.reset_mock() + eWeLink_cluster_level.request.reset_mock() + + # test 0 length transition with brightness, but no color provided dev1_cluster_on_off.request.reset_mock() dev1_cluster_level.request.reset_mock() await hass.services.async_call( @@ -1423,18 +1494,10 @@ async def async_test_level_on_off_from_hass( {"entity_id": entity_id, "transition": 10}, blocking=True, ) - assert on_off_cluster.request.call_count == 1 - assert on_off_cluster.request.await_count == 1 + assert on_off_cluster.request.call_count == 0 + assert on_off_cluster.request.await_count == 0 assert level_cluster.request.call_count == 1 assert level_cluster.request.await_count == 1 - assert on_off_cluster.request.call_args == call( - False, - on_off_cluster.commands_by_name["on"].id, - on_off_cluster.commands_by_name["on"].schema, - expect_reply=True, - manufacturer=None, - tsn=None, - ) assert level_cluster.request.call_args == call( False, level_cluster.commands_by_name["move_to_level_with_on_off"].id,