diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 3296f33f84c..1adcc269eb9 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -197,7 +197,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2023.08.0 + uses: home-assistant/builder@2023.09.0 with: args: | $BUILD_ARGS \ @@ -205,8 +205,6 @@ jobs: --cosign \ --target /data \ --generic ${{ needs.init.outputs.version }} - env: - CAS_API_KEY: ${{ secrets.CAS_TOKEN }} - name: Archive translations shell: bash @@ -275,15 +273,13 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2023.08.0 + uses: home-assistant/builder@2023.09.0 with: args: | $BUILD_ARGS \ --target /data/machine \ --cosign \ --machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}" - env: - CAS_API_KEY: ${{ secrets.CAS_TOKEN }} publish_ha: name: Publish version files diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 01823199c17..c6f819f9dfd 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -97,7 +97,7 @@ jobs: name: requirements_diff - name: Build wheels - uses: home-assistant/wheels@2023.04.0 + uses: home-assistant/wheels@2023.09.1 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 @@ -178,7 +178,7 @@ jobs: sed -i "/numpy/d" homeassistant/package_constraints.txt - name: Build wheels (part 1) - uses: home-assistant/wheels@2023.04.0 + uses: home-assistant/wheels@2023.09.1 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 @@ -192,7 +192,7 @@ jobs: requirements: "requirements_all.txtaa" - name: Build wheels (part 2) - uses: home-assistant/wheels@2023.04.0 + uses: home-assistant/wheels@2023.09.1 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 @@ -206,7 +206,7 @@ jobs: requirements: "requirements_all.txtab" - name: Build wheels (part 3) - uses: home-assistant/wheels@2023.04.0 + uses: home-assistant/wheels@2023.09.1 with: abi: ${{ matrix.abi }} tag: musllinux_1_2 diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index 09393741d63..c83232c273a 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -58,6 +58,16 @@ class AirNowEntityDescription(SensorEntityDescription, AirNowEntityDescriptionMi """Describes Airnow sensor entity.""" +def station_extra_attrs(data: dict[str, Any]) -> dict[str, Any]: + """Process extra attributes for station location (if available).""" + if ATTR_API_STATION in data: + return { + "lat": data.get(ATTR_API_STATION_LATITUDE), + "long": data.get(ATTR_API_STATION_LONGITUDE), + } + return {} + + SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = ( AirNowEntityDescription( key=ATTR_API_AQI, @@ -93,10 +103,7 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = ( translation_key="station", icon="mdi:blur", value_fn=lambda data: data.get(ATTR_API_STATION), - extra_state_attributes_fn=lambda data: { - "lat": data[ATTR_API_STATION_LATITUDE], - "long": data[ATTR_API_STATION_LONGITUDE], - }, + extra_state_attributes_fn=station_extra_attrs, ), ) diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index b66d6b8f810..28b5fa3a7a6 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -144,7 +144,8 @@ def async_migrate(hass: HomeAssistant, address: str, sensor_name: str) -> None: not matching_reg_entry or "(" not in entry.unique_id ): matching_reg_entry = entry - if not matching_reg_entry: + if not matching_reg_entry or matching_reg_entry.unique_id == new_unique_id: + # Already has the newest unique id format return entity_id = matching_reg_entry.entity_id ent_reg.async_update_entity(entity_id=entity_id, new_unique_id=new_unique_id) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 520daa9f5c2..f4d060ed7b8 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -298,6 +298,26 @@ class Pipeline: id: str = field(default_factory=ulid_util.ulid) + @classmethod + def from_json(cls, data: dict[str, Any]) -> Pipeline: + """Create an instance from a JSON serialization. + + This function was added in HA Core 2023.10, previous versions will raise + if there are unexpected items in the serialized data. + """ + return cls( + conversation_engine=data["conversation_engine"], + conversation_language=data["conversation_language"], + id=data["id"], + language=data["language"], + name=data["name"], + stt_engine=data["stt_engine"], + stt_language=data["stt_language"], + tts_engine=data["tts_engine"], + tts_language=data["tts_language"], + tts_voice=data["tts_voice"], + ) + def to_json(self) -> dict[str, Any]: """Return a JSON serializable representation for storage.""" return { @@ -1205,7 +1225,7 @@ class PipelineStorageCollection( def _deserialize_item(self, data: dict) -> Pipeline: """Create an item from its serialized representation.""" - return Pipeline(**data) + return Pipeline.from_json(data) def _serialize_item(self, item_id: str, item: Pipeline) -> dict: """Return the serialized representation of an item for storing.""" diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index cd2737adca3..c5a0da71136 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==1.8.0", "yalexs-ble==2.2.3"] + "requirements": ["yalexs==1.9.0", "yalexs-ble==2.3.0"] } diff --git a/homeassistant/components/comelit/config_flow.py b/homeassistant/components/comelit/config_flow.py index dd6227a6583..b0c8e5aabe5 100644 --- a/homeassistant/components/comelit/config_flow.py +++ b/homeassistant/components/comelit/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any -from aiocomelit import ComeliteSerialBridgeAPi, exceptions as aiocomelit_exceptions +from aiocomelit import ComeliteSerialBridgeApi, exceptions as aiocomelit_exceptions import voluptuous as vol from homeassistant import core, exceptions @@ -37,7 +37,7 @@ async def validate_input( ) -> dict[str, str]: """Validate the user input allows us to connect.""" - api = ComeliteSerialBridgeAPi(data[CONF_HOST], data[CONF_PIN]) + api = ComeliteSerialBridgeApi(data[CONF_HOST], data[CONF_PIN]) try: await api.login() diff --git a/homeassistant/components/comelit/coordinator.py b/homeassistant/components/comelit/coordinator.py index 1affd5046fe..df1d745ce8a 100644 --- a/homeassistant/components/comelit/coordinator.py +++ b/homeassistant/components/comelit/coordinator.py @@ -3,11 +3,14 @@ import asyncio from datetime import timedelta from typing import Any -from aiocomelit import ComeliteSerialBridgeAPi +from aiocomelit import ComeliteSerialBridgeApi, ComelitSerialBridgeObject +from aiocomelit.const import BRIDGE import aiohttp +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import _LOGGER, DOMAIN @@ -16,13 +19,15 @@ from .const import _LOGGER, DOMAIN class ComelitSerialBridge(DataUpdateCoordinator): """Queries Comelit Serial Bridge.""" + config_entry: ConfigEntry + def __init__(self, hass: HomeAssistant, host: str, pin: int) -> None: """Initialize the scanner.""" self._host = host self._pin = pin - self.api = ComeliteSerialBridgeAPi(host, pin) + self.api = ComeliteSerialBridgeApi(host, pin) super().__init__( hass=hass, @@ -30,6 +35,38 @@ class ComelitSerialBridge(DataUpdateCoordinator): name=f"{DOMAIN}-{host}-coordinator", update_interval=timedelta(seconds=5), ) + device_registry = dr.async_get(self.hass) + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + identifiers={(DOMAIN, self.config_entry.entry_id)}, + model=BRIDGE, + name=f"{BRIDGE} ({self.api.host})", + **self.basic_device_info, + ) + + @property + def basic_device_info(self) -> dict: + """Set basic device info.""" + + return { + "manufacturer": "Comelit", + "hw_version": "20003101", + } + + def platform_device_info( + self, device: ComelitSerialBridgeObject, platform: str + ) -> dr.DeviceInfo: + """Set platform device info.""" + + return dr.DeviceInfo( + identifiers={ + (DOMAIN, f"{self.config_entry.entry_id}-{platform}-{device.index}") + }, + via_device=(DOMAIN, self.config_entry.entry_id), + name=device.name, + model=f"{BRIDGE} {platform}", + **self.basic_device_info, + ) async def _async_update_data(self) -> dict[str, Any]: """Update router data.""" diff --git a/homeassistant/components/comelit/light.py b/homeassistant/components/comelit/light.py index 9a893bd929c..a4a534025f0 100644 --- a/homeassistant/components/comelit/light.py +++ b/homeassistant/components/comelit/light.py @@ -9,7 +9,6 @@ from aiocomelit.const import LIGHT, LIGHT_OFF, LIGHT_ON from homeassistant.components.light import LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -37,27 +36,20 @@ class ComelitLightEntity(CoordinatorEntity[ComelitSerialBridge], LightEntity): """Light device.""" _attr_has_entity_name = True - _attr_name = None def __init__( self, coordinator: ComelitSerialBridge, device: ComelitSerialBridgeObject, - config_entry_unique_id: str | None, + config_entry_unique_id: str, ) -> None: """Init light entity.""" self._api = coordinator.api self._device = device super().__init__(coordinator) + self._attr_name = device.name self._attr_unique_id = f"{config_entry_unique_id}-{device.index}" - self._attr_device_info = DeviceInfo( - identifiers={ - (DOMAIN, self._attr_unique_id), - }, - manufacturer="Comelit", - model="Serial Bridge", - name=device.name, - ) + self._attr_device_info = self.coordinator.platform_device_info(device, LIGHT) async def _light_set_state(self, state: int) -> None: """Set desired light state.""" diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json index fc7f2a3fc12..ee876434825 100644 --- a/homeassistant/components/comelit/manifest.json +++ b/homeassistant/components/comelit/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/comelit", "iot_class": "local_polling", "loggers": ["aiocomelit"], - "requirements": ["aiocomelit==0.0.5"] + "requirements": ["aiocomelit==0.0.8"] } diff --git a/homeassistant/components/comelit/strings.json b/homeassistant/components/comelit/strings.json index 6508f58412e..436fbfd5aec 100644 --- a/homeassistant/components/comelit/strings.json +++ b/homeassistant/components/comelit/strings.json @@ -3,7 +3,7 @@ "flow_title": "{host}", "step": { "reauth_confirm": { - "description": "Please enter the correct PIN for VEDO system: {host}", + "description": "Please enter the correct PIN for {host}", "data": { "pin": "[%key:common::config_flow::data::pin%]" } diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 9e0909b6dfc..2f733ead486 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.8.2"] + "requirements": ["hassil==1.2.5", "home-assistant-intents==2023.9.22"] } diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index f54fddc9a86..627a121dcb4 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -74,7 +74,7 @@ async def async_setup_entry( # noqa: C901 """Fetch data from API endpoint.""" assert device.device try: - async with asyncio.timeout(10): + async with asyncio.timeout(30): return await device.device.async_check_firmware_available() except DeviceUnavailable as err: raise UpdateFailed(err) from err diff --git a/homeassistant/components/devolo_home_network/manifest.json b/homeassistant/components/devolo_home_network/manifest.json index a047437e980..27fd08898c0 100644 --- a/homeassistant/components/devolo_home_network/manifest.json +++ b/homeassistant/components/devolo_home_network/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["devolo_plc_api"], "quality_scale": "platinum", - "requirements": ["devolo-plc-api==1.4.0"], + "requirements": ["devolo-plc-api==1.4.1"], "zeroconf": [ { "type": "_dvl-deviceapi._tcp.local.", diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index b18f646add7..e1253b585ac 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -326,6 +326,7 @@ class Thermostat(ClimateEntity): self._attr_unique_id = self.thermostat["identifier"] self.vacation = None self._last_active_hvac_mode = HVACMode.HEAT_COOL + self._last_hvac_mode_before_aux_heat = HVACMode.HEAT_COOL self._attr_hvac_modes = [] if self.settings["heatStages"] or self.settings["hasHeatPump"]: @@ -541,13 +542,14 @@ class Thermostat(ClimateEntity): def turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" _LOGGER.debug("Setting HVAC mode to auxHeatOnly to turn on aux heat") + self._last_hvac_mode_before_aux_heat = self.hvac_mode self.data.ecobee.set_hvac_mode(self.thermostat_index, ECOBEE_AUX_HEAT_ONLY) self.update_without_throttle = True def turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" _LOGGER.debug("Setting HVAC mode to last mode to disable aux heat") - self.set_hvac_mode(self._last_active_hvac_mode) + self.set_hvac_mode(self._last_hvac_mode_before_aux_heat) self.update_without_throttle = True def set_preset_mode(self, preset_mode: str) -> None: diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index c6d127a3f6e..917e325be51 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.11.0"], + "requirements": ["pyenphase==1.11.4"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 8d52115d49b..d8d8f6b94bf 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/fritz", "iot_class": "local_polling", "loggers": ["fritzconnection"], - "requirements": ["fritzconnection[qr]==1.12.2", "xmltodict==0.13.0"], + "requirements": ["fritzconnection[qr]==1.13.2", "xmltodict==0.13.0"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index c3c305ab07e..4e5c60091c9 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["fritzconnection"], - "requirements": ["fritzconnection[qr]==1.12.2"] + "requirements": ["fritzconnection[qr]==1.13.2"] } diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index 43cdb29f85f..c8902622f85 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -189,7 +189,11 @@ class FritzBoxCallMonitor: _LOGGER.debug("Setting up socket connection") try: self.connection = FritzMonitor(address=self.host, port=self.port) - kwargs: dict[str, Any] = {"event_queue": self.connection.start()} + kwargs: dict[str, Any] = { + "event_queue": self.connection.start( + reconnect_tries=50, reconnect_delay=120 + ) + } Thread(target=self._process_events, kwargs=kwargs).start() except OSError as err: self.connection = None diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index a5bb3981575..f9b93cbe954 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -103,10 +103,7 @@ class IPMAWeather(WeatherEntity, IPMADevice): else: self._daily_forecast = None - if self._period == 1 or self._forecast_listeners["hourly"]: - await self._update_forecast("hourly", 1, True) - else: - self._hourly_forecast = None + await self._update_forecast("hourly", 1, True) _LOGGER.debug( "Updated location %s based on %s, current observation %s", @@ -139,8 +136,8 @@ class IPMAWeather(WeatherEntity, IPMADevice): @property def condition(self): - """Return the current condition.""" - forecast = self._hourly_forecast or self._daily_forecast + """Return the current condition which is only available on the hourly forecast data.""" + forecast = self._hourly_forecast if not forecast: return diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 98cb4665614..dd8fd4af83b 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -196,9 +196,9 @@ async def async_setup_entry( data = hass.data[DOMAIN][entry.entry_id] coordinator_forecast: DataUpdateCoordinator[Forecast] = data[COORDINATOR_FORECAST] coordinator_rain: DataUpdateCoordinator[Rain] | None = data[COORDINATOR_RAIN] - coordinator_alert: DataUpdateCoordinator[CurrentPhenomenons] | None = data[ + coordinator_alert: DataUpdateCoordinator[CurrentPhenomenons] | None = data.get( COORDINATOR_ALERT - ] + ) entities: list[MeteoFranceSensor[Any]] = [ MeteoFranceSensor(coordinator_forecast, description) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index a4c824b3674..7e9416f6695 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mill", "iot_class": "local_polling", "loggers": ["mill", "mill_local"], - "requirements": ["millheater==0.11.2", "mill-local==0.2.0"] + "requirements": ["millheater==0.11.5", "mill-local==0.2.0"] } diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 2a726075bb0..9a1600f5865 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -462,6 +462,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received) + @callback def _rgbx_received( msg: ReceiveMessage, template: str, @@ -532,11 +533,26 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): @log_messages(self.hass, self.entity_id) def rgbww_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for RGBWW.""" + + @callback + def _converter( + r: int, g: int, b: int, cw: int, ww: int + ) -> tuple[int, int, int]: + min_kelvin = color_util.color_temperature_mired_to_kelvin( + self.max_mireds + ) + max_kelvin = color_util.color_temperature_mired_to_kelvin( + self.min_mireds + ) + return color_util.color_rgbww_to_rgb( + r, g, b, cw, ww, min_kelvin, max_kelvin + ) + rgbww = _rgbx_received( msg, CONF_RGBWW_VALUE_TEMPLATE, ColorMode.RGBWW, - color_util.color_rgbww_to_rgb, + _converter, ) if rgbww is None: return diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index da260a2559e..7b74880d011 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -190,8 +190,6 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) except CannotLoginException: errors["base"] = "config" - - if errors: return await self._show_setup_form(user_input, errors) config_data = { @@ -204,6 +202,10 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Check if already configured info = await self.hass.async_add_executor_job(api.get_info) + if info is None: + errors["base"] = "info" + return await self._show_setup_form(user_input, errors) + await self.async_set_unique_id(info["SerialNumber"], raise_on_progress=False) self._abort_if_unique_id_configured(updates=config_data) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index be4dd0f2d9d..59a41542d7c 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/netgear", "iot_class": "local_polling", "loggers": ["pynetgear"], - "requirements": ["pynetgear==0.10.9"], + "requirements": ["pynetgear==0.10.10"], "ssdp": [ { "manufacturer": "NETGEAR, Inc.", diff --git a/homeassistant/components/netgear/strings.json b/homeassistant/components/netgear/strings.json index 7941d1fe0a7..15766874bc5 100644 --- a/homeassistant/components/netgear/strings.json +++ b/homeassistant/components/netgear/strings.json @@ -11,7 +11,8 @@ } }, "error": { - "config": "Connection or login error: please check your configuration" + "config": "Connection or login error: please check your configuration", + "info": "Failed to get info from router" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/rainbird/config_flow.py b/homeassistant/components/rainbird/config_flow.py index a784e4623d6..bf6682e7a6f 100644 --- a/homeassistant/components/rainbird/config_flow.py +++ b/homeassistant/components/rainbird/config_flow.py @@ -125,8 +125,13 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): options: dict[str, Any], ) -> FlowResult: """Create the config entry.""" - await self.async_set_unique_id(serial_number) - self._abort_if_unique_id_configured() + # Prevent devices with the same serial number. If the device does not have a serial number + # then we can at least prevent configuring the same host twice. + if serial_number: + await self.async_set_unique_id(serial_number) + self._abort_if_unique_id_configured() + else: + self._async_abort_entries_match(data) return self.async_create_entry( title=data[CONF_HOST], data=data, diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index a43dbce9a7c..2487013b032 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -61,7 +61,8 @@ class ReolinkHost: ) self.webhook_id: str | None = None - self._onvif_supported: bool = True + self._onvif_push_supported: bool = True + self._onvif_long_poll_supported: bool = True self._base_url: str = "" self._webhook_url: str = "" self._webhook_reachable: bool = False @@ -97,7 +98,9 @@ class ReolinkHost: f"'{self._api.user_level}', only admin users can change camera settings" ) - self._onvif_supported = self._api.supported(None, "ONVIF") + onvif_supported = self._api.supported(None, "ONVIF") + self._onvif_push_supported = onvif_supported + self._onvif_long_poll_supported = onvif_supported enable_rtsp = None enable_onvif = None @@ -109,7 +112,7 @@ class ReolinkHost: ) enable_rtsp = True - if not self._api.onvif_enabled and self._onvif_supported: + if not self._api.onvif_enabled and onvif_supported: _LOGGER.debug( "ONVIF is disabled on %s, trying to enable it", self._api.nvr_name ) @@ -157,11 +160,11 @@ class ReolinkHost: self._unique_id = format_mac(self._api.mac_address) - if self._onvif_supported: + if self._onvif_push_supported: try: await self.subscribe() except NotSupportedError: - self._onvif_supported = False + self._onvif_push_supported = False self.unregister_webhook() await self._api.unsubscribe() else: @@ -179,12 +182,27 @@ class ReolinkHost: self._cancel_onvif_check = async_call_later( self._hass, FIRST_ONVIF_TIMEOUT, self._async_check_onvif ) - if not self._onvif_supported: + if not self._onvif_push_supported: _LOGGER.debug( - "Camera model %s does not support ONVIF, using fast polling instead", + "Camera model %s does not support ONVIF push, using ONVIF long polling instead", self._api.model, ) - await self._async_poll_all_motion() + try: + await self._async_start_long_polling(initial=True) + except NotSupportedError: + _LOGGER.debug( + "Camera model %s does not support ONVIF long polling, using fast polling instead", + self._api.model, + ) + self._onvif_long_poll_supported = False + await self._api.unsubscribe() + await self._async_poll_all_motion() + else: + self._cancel_long_poll_check = async_call_later( + self._hass, + FIRST_ONVIF_LONG_POLL_TIMEOUT, + self._async_check_onvif_long_poll, + ) if self._api.sw_version_update_required: ir.async_create_issue( @@ -317,11 +335,22 @@ class ReolinkHost: str(err), ) - async def _async_start_long_polling(self): + async def _async_start_long_polling(self, initial=False): """Start ONVIF long polling task.""" if self._long_poll_task is None: try: await self._api.subscribe(sub_type=SubType.long_poll) + except NotSupportedError as err: + if initial: + raise err + # make sure the long_poll_task is always created to try again later + if not self._lost_subscription: + self._lost_subscription = True + _LOGGER.error( + "Reolink %s event long polling subscription lost: %s", + self._api.nvr_name, + str(err), + ) except ReolinkError as err: # make sure the long_poll_task is always created to try again later if not self._lost_subscription: @@ -381,12 +410,11 @@ class ReolinkHost: async def renew(self) -> None: """Renew the subscription of motion events (lease time is 15 minutes).""" - if not self._onvif_supported: - return - try: - await self._renew(SubType.push) - if self._long_poll_task is not None: + if self._onvif_push_supported: + await self._renew(SubType.push) + + if self._onvif_long_poll_supported and self._long_poll_task is not None: if not self._api.subscribed(SubType.long_poll): _LOGGER.debug("restarting long polling task") # To prevent 5 minute request timeout diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 060490c6e56..221a6b8b59d 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.7.9"] + "requirements": ["reolink-aio==0.7.10"] } diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 95aa26a1ff5..15ba4baed45 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -223,6 +223,7 @@ "state": { "off": "[%key:common::state::off%]", "auto": "Auto", + "onatnight": "On at night", "schedule": "Schedule", "adaptive": "Adaptive", "autoadaptive": "Auto adaptive" diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 355c630272e..0b5198f36d3 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/ring", "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], - "requirements": ["ring-doorbell==0.7.2"] + "requirements": ["ring-doorbell==0.7.3"] } diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index 27f25208a4e..2b005ecade6 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -40,7 +40,7 @@ class RoborockEntity(Entity): async def send( self, - command: RoborockCommand, + command: RoborockCommand | str, params: dict[str, Any] | list[Any] | int | None = None, ) -> dict: """Send a command to a vacuum cleaner.""" @@ -48,7 +48,7 @@ class RoborockEntity(Entity): response = await self._api.send_command(command, params) except RoborockException as err: raise HomeAssistantError( - f"Error while calling {command.name} with {params}" + f"Error while calling {command.name if isinstance(command, RoborockCommand) else command} with {params}" ) from err return response diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index dfcac67d2b0..dfd5a9ee1c7 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.33.2"] + "requirements": ["python-roborock==0.34.1"] } diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 5ca2292f804..20e90488ad3 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -164,10 +164,10 @@ "dnd_end_time": { "name": "Do not disturb end" }, - "off_peak_start_time": { + "off_peak_start": { "name": "Off-peak start" }, - "off_peak_end_time": { + "off_peak_end": { "name": "Off-peak end" } }, diff --git a/homeassistant/components/schlage/manifest.json b/homeassistant/components/schlage/manifest.json index 25316004c58..3568692c6ca 100644 --- a/homeassistant/components/schlage/manifest.json +++ b/homeassistant/components/schlage/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/schlage", "iot_class": "cloud_polling", - "requirements": ["pyschlage==2023.8.1"] + "requirements": ["pyschlage==2023.9.1"] } diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index da86ba8fe24..2e2b92179f0 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -54,7 +54,15 @@ ATTR_HORIZONTAL_SWING_MODE = "horizontal_swing_mode" ATTR_LIGHT = "light" BOOST_INCLUSIVE = "boost_inclusive" -AVAILABLE_FAN_MODES = {"quiet", "low", "medium", "medium_high", "high", "auto"} +AVAILABLE_FAN_MODES = { + "quiet", + "low", + "medium", + "medium_high", + "high", + "strong", + "auto", +} AVAILABLE_SWING_MODES = { "stopped", "fixedtop", diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index a6f14b73ace..ddd164225fc 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -127,6 +127,7 @@ "low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]", "medium": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]", "medium_high": "Medium high", + "strong": "Strong", "quiet": "Quiet" } }, @@ -211,6 +212,7 @@ "low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]", "medium": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]", "medium_high": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::fanlevel::state::medium_high%]", + "strong": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::fanlevel::state::strong%]", "quiet": "[%key:component::sensibo::entity::sensor::climate_react_low::state_attributes::fanlevel::state::quiet%]" } }, @@ -347,6 +349,7 @@ "fan_mode": { "state": { "quiet": "Quiet", + "strong": "Strong", "low": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::low%]", "medium": "[%key:component::climate::entity_component::_::state_attributes::fan_mode::state::medium%]", "medium_high": "Medium high", diff --git a/homeassistant/components/sensirion_ble/manifest.json b/homeassistant/components/sensirion_ble/manifest.json index 38f66a88e8e..01ccc873f56 100644 --- a/homeassistant/components/sensirion_ble/manifest.json +++ b/homeassistant/components/sensirion_ble/manifest.json @@ -16,5 +16,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/sensirion_ble", "iot_class": "local_push", - "requirements": ["sensirion-ble==0.1.0"] + "requirements": ["sensirion-ble==0.1.1"] } diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index b8151256519..446dd60fd92 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -345,7 +345,7 @@ class SensorEntity(Entity): """Return initial entity options. These will be stored in the entity registry the first time the entity is seen, - and then never updated. + and then only updated if the unit system is changed. """ suggested_unit_of_measurement = self._get_initial_suggested_unit() @@ -783,7 +783,7 @@ class SensorEntity(Entity): registry = er.async_get(self.hass) initial_options = self.get_initial_entity_options() or {} registry.async_update_entity_options( - self.entity_id, + self.registry_entry.entity_id, f"{DOMAIN}.private", initial_options.get(f"{DOMAIN}.private"), ) diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index a04fc7a641d..d815655d775 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -44,7 +44,12 @@ from homeassistant.util.unit_conversion import ( from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf -CHECK_FORECAST_KEYS = set().union(Forecast.__annotations__.keys()) +CHECK_FORECAST_KEYS = ( + set().union(Forecast.__annotations__.keys()) + # Manually add the forecast resulting attributes that only exists + # as native_* in the Forecast definition + .union(("apparent_temperature", "wind_gust_speed", "dew_point")) +) CONDITION_CLASSES = { ATTR_CONDITION_CLEAR_NIGHT, @@ -434,7 +439,8 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): diff_result = set().union(forecast.keys()).difference(CHECK_FORECAST_KEYS) if diff_result: raise vol.Invalid( - "Only valid keys in Forecast are allowed, see Weather documentation https://www.home-assistant.io/integrations/weather/" + f"Only valid keys in Forecast are allowed, unallowed keys: ({diff_result}), " + "see Weather documentation https://www.home-assistant.io/integrations/weather/" ) if forecast_type == "twice_daily" and "is_daytime" not in forecast: raise vol.Invalid( diff --git a/homeassistant/components/timer/services.yaml b/homeassistant/components/timer/services.yaml index 74eeae22b23..ac5453d38c9 100644 --- a/homeassistant/components/timer/services.yaml +++ b/homeassistant/components/timer/services.yaml @@ -36,3 +36,5 @@ change: example: "00:01:00, 60 or -60" selector: text: + +reload: diff --git a/homeassistant/components/timer/strings.json b/homeassistant/components/timer/strings.json index 56cb46d26b4..1ebf0c6f50a 100644 --- a/homeassistant/components/timer/strings.json +++ b/homeassistant/components/timer/strings.json @@ -62,6 +62,10 @@ "description": "Duration to add or subtract to the running timer." } } + }, + "reload": { + "name": "[%key:common::action::reload%]", + "description": "Reloads timers from the YAML-configuration." } } } diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py index 77675e3f2ec..626049276f5 100644 --- a/homeassistant/components/tomorrowio/__init__.py +++ b/homeassistant/components/tomorrowio/__init__.py @@ -221,7 +221,6 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): await self.async_refresh() self.update_interval = async_set_update_interval(self.hass, self._api) - self._next_refresh = None self._async_unsub_refresh() if self._listeners: self._schedule_refresh() diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index f3e86136f5d..cd581d8c37f 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -17,6 +17,7 @@ from homeassistant.components.sensor import ( SensorExtraStoredData, SensorStateClass, ) +from homeassistant.components.sensor.recorder import _suggest_report_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -484,6 +485,12 @@ class UtilityMeterSensor(RestoreSensor): DATA_TARIFF_SENSORS ]: sensor.start(new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) + if self._unit_of_measurement is None: + _LOGGER.warning( + "Source sensor %s has no unit of measurement. Please %s", + self._sensor_source_id, + _suggest_report_issue(self.hass, self._sensor_source_id), + ) if ( adjustment := self.calculate_adjustment(old_state, new_state) @@ -491,6 +498,7 @@ class UtilityMeterSensor(RestoreSensor): # If net_consumption is off, the adjustment must be non-negative self._state += adjustment # type: ignore[operator] # self._state will be set to by the start function if it is None, therefore it always has a valid Decimal value at this line + self._unit_of_measurement = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._last_valid_state = new_state_val self.async_write_ha_state() diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index c72d9b1dbad..1a4be798367 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "iot_class": "cloud_polling", "loggers": ["pywaze", "homeassistant.helpers.location"], - "requirements": ["pywaze==0.4.0"] + "requirements": ["pywaze==0.5.0"] } diff --git a/homeassistant/components/xiaomi_miio/button.py b/homeassistant/components/xiaomi_miio/button.py index 9ed9b780911..e5e11b85e58 100644 --- a/homeassistant/components/xiaomi_miio/button.py +++ b/homeassistant/components/xiaomi_miio/button.py @@ -169,8 +169,12 @@ class XiaomiGenericCoordinatedButton(XiaomiCoordinatedMiioEntity, ButtonEntity): async def async_press(self) -> None: """Press the button.""" method = getattr(self._device, self.entity_description.method_press) - await self._try_command( - self.entity_description.method_press_error_message, - method, - self.entity_description.method_press_params, - ) + params = self.entity_description.method_press_params + if params is not None: + await self._try_command( + self.entity_description.method_press_error_message, method, params + ) + else: + await self._try_command( + self.entity_description.method_press_error_message, method + ) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 3aefeea048a..cbff581d296 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", "iot_class": "local_push", - "requirements": ["yalexs-ble==2.2.3"] + "requirements": ["yalexs-ble==2.3.0"] } diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index ced0d527c7d..7322c58ae04 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/yolink", "iot_class": "cloud_push", - "requirements": ["yolink-api==0.3.0"] + "requirements": ["yolink-api==0.3.1"] } diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index c3fa6b1ff01..3610cd41425 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.36.3", + "bellows==0.36.4", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.103", @@ -30,7 +30,7 @@ "zigpy-xbee==0.18.2", "zigpy-zigate==0.11.0", "zigpy-znp==0.11.4", - "universal-silabs-flasher==0.0.13" + "universal-silabs-flasher==0.0.14" ], "usb": [ { diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 4ea46099f14..cfb2c239d8e 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["zwave_js_server"], "quality_scale": "platinum", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.51.2"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.51.3"], "usb": [ { "vid": "0658", diff --git a/homeassistant/const.py b/homeassistant/const.py index 08e12ce58ff..8ce4d434083 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 = 9 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __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/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 34651fcaf9d..2b570009a57 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -81,7 +81,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): self._shutdown_requested = False self.config_entry = config_entries.current_entry.get() self.always_update = always_update - self._next_refresh: float | None = None # It's None before the first successful update. # Components should call async_config_entry_first_refresh @@ -184,7 +183,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): """Unschedule any pending refresh since there is no longer any listeners.""" self._async_unsub_refresh() self._debounced_refresh.async_cancel() - self._next_refresh = None def async_contexts(self) -> Generator[Any, None, None]: """Return all registered contexts.""" @@ -220,13 +218,13 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): # We use event.async_call_at because DataUpdateCoordinator does # not need an exact update interval. now = self.hass.loop.time() - if self._next_refresh is None or self._next_refresh <= now: - self._next_refresh = int(now) + self._microsecond - self._next_refresh += self.update_interval.total_seconds() + + next_refresh = int(now) + self._microsecond + next_refresh += self.update_interval.total_seconds() self._unsub_refresh = event.async_call_at( self.hass, self._job, - self._next_refresh, + next_refresh, ) async def _handle_refresh_interval(self, _now: datetime) -> None: @@ -265,7 +263,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): async def async_refresh(self) -> None: """Refresh data and log errors.""" - self._next_refresh = None await self._async_refresh(log_failures=True) async def _async_refresh( # noqa: C901 @@ -405,7 +402,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): """Manually update data, notify listeners and reset refresh interval.""" self._async_unsub_refresh() self._debounced_refresh.async_cancel() - self._next_refresh = None self.data = data self.last_update_success = True diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 343c3be1481..714c11baf4a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ hass-nabucasa==0.71.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 home-assistant-frontend==20230911.0 -home-assistant-intents==2023.8.2 +home-assistant-intents==2023.9.22 httpx==0.24.1 ifaddr==0.2.0 janus==1.0.0 diff --git a/pyproject.toml b/pyproject.toml index 53f9bf38a32..de21d99335f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.2" +version = "2023.9.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" diff --git a/requirements_all.txt b/requirements_all.txt index f4315943472..e9eaf4acdc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -209,7 +209,7 @@ aiobafi6==0.9.0 aiobotocore==2.6.0 # homeassistant.components.comelit -aiocomelit==0.0.5 +aiocomelit==0.0.8 # homeassistant.components.dhcp aiodiscover==1.4.16 @@ -509,7 +509,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.36.3 +bellows==0.36.4 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.0 @@ -670,7 +670,7 @@ denonavr==0.11.3 devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network -devolo-plc-api==1.4.0 +devolo-plc-api==1.4.1 # homeassistant.components.directv directv==0.4.0 @@ -829,7 +829,7 @@ freesms==0.2.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection[qr]==1.12.2 +fritzconnection[qr]==1.13.2 # homeassistant.components.google_translate gTTS==2.2.4 @@ -997,7 +997,7 @@ holidays==0.28 home-assistant-frontend==20230911.0 # homeassistant.components.conversation -home-assistant-intents==2023.8.2 +home-assistant-intents==2023.9.22 # homeassistant.components.home_connect homeconnect==0.7.2 @@ -1213,7 +1213,7 @@ micloud==0.5 mill-local==0.2.0 # homeassistant.components.mill -millheater==0.11.2 +millheater==0.11.5 # homeassistant.components.minio minio==7.1.12 @@ -1671,7 +1671,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.11.0 +pyenphase==1.11.4 # homeassistant.components.envisalink pyenvisalink==4.6 @@ -1866,7 +1866,7 @@ pymyq==3.1.4 pymysensors==0.24.0 # homeassistant.components.netgear -pynetgear==0.10.9 +pynetgear==0.10.10 # homeassistant.components.netio pynetio==0.1.9.1 @@ -1988,7 +1988,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.schlage -pyschlage==2023.8.1 +pyschlage==2023.9.1 # homeassistant.components.sensibo pysensibo==1.0.33 @@ -2159,7 +2159,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.33.2 +python-roborock==0.34.1 # homeassistant.components.smarttub python-smarttub==0.0.33 @@ -2231,7 +2231,7 @@ pyvlx==0.2.20 pyvolumio==0.1.5 # homeassistant.components.waze_travel_time -pywaze==0.4.0 +pywaze==0.5.0 # homeassistant.components.html5 pywebpush==1.9.2 @@ -2294,7 +2294,7 @@ renault-api==0.2.0 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.9 +reolink-aio==0.7.10 # homeassistant.components.idteck_prox rfk101py==0.0.1 @@ -2303,7 +2303,7 @@ rfk101py==0.0.1 rflink==0.0.65 # homeassistant.components.ring -ring-doorbell==0.7.2 +ring-doorbell==0.7.3 # homeassistant.components.fleetgo ritassist==0.9.2 @@ -2375,7 +2375,7 @@ sense-energy==0.12.1 sense_energy==0.12.1 # homeassistant.components.sensirion_ble -sensirion-ble==0.1.0 +sensirion-ble==0.1.1 # homeassistant.components.sensorpro sensorpro-ble==0.5.3 @@ -2612,7 +2612,7 @@ unifi-discovery==1.1.7 unifiled==0.11 # homeassistant.components.zha -universal-silabs-flasher==0.0.13 +universal-silabs-flasher==0.0.14 # homeassistant.components.upb upb-lib==0.5.4 @@ -2736,10 +2736,10 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.2.3 +yalexs-ble==2.3.0 # homeassistant.components.august -yalexs==1.8.0 +yalexs==1.9.0 # homeassistant.components.yeelight yeelight==0.7.13 @@ -2748,7 +2748,7 @@ yeelight==0.7.13 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.3.0 +yolink-api==0.3.1 # homeassistant.components.youless youless-api==1.0.1 @@ -2799,7 +2799,7 @@ zigpy==0.57.1 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.51.2 +zwave-js-server-python==0.51.3 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cdfdefdb989..e1c571d6472 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -190,7 +190,7 @@ aiobafi6==0.9.0 aiobotocore==2.6.0 # homeassistant.components.comelit -aiocomelit==0.0.5 +aiocomelit==0.0.8 # homeassistant.components.dhcp aiodiscover==1.4.16 @@ -430,7 +430,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.36.3 +bellows==0.36.4 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.0 @@ -544,7 +544,7 @@ denonavr==0.11.3 devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network -devolo-plc-api==1.4.0 +devolo-plc-api==1.4.1 # homeassistant.components.directv directv==0.4.0 @@ -648,7 +648,7 @@ freebox-api==1.1.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection[qr]==1.12.2 +fritzconnection[qr]==1.13.2 # homeassistant.components.google_translate gTTS==2.2.4 @@ -780,7 +780,7 @@ holidays==0.28 home-assistant-frontend==20230911.0 # homeassistant.components.conversation -home-assistant-intents==2023.8.2 +home-assistant-intents==2023.9.22 # homeassistant.components.home_connect homeconnect==0.7.2 @@ -927,7 +927,7 @@ micloud==0.5 mill-local==0.2.0 # homeassistant.components.mill -millheater==0.11.2 +millheater==0.11.5 # homeassistant.components.minio minio==7.1.12 @@ -1235,7 +1235,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.11.0 +pyenphase==1.11.4 # homeassistant.components.everlights pyeverlights==0.1.0 @@ -1382,7 +1382,7 @@ pymyq==3.1.4 pymysensors==0.24.0 # homeassistant.components.netgear -pynetgear==0.10.9 +pynetgear==0.10.10 # homeassistant.components.nobo_hub pynobo==1.6.0 @@ -1477,7 +1477,7 @@ pyrympro==0.0.7 pysabnzbd==1.1.1 # homeassistant.components.schlage -pyschlage==2023.8.1 +pyschlage==2023.9.1 # homeassistant.components.sensibo pysensibo==1.0.33 @@ -1585,7 +1585,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.33.2 +python-roborock==0.34.1 # homeassistant.components.smarttub python-smarttub==0.0.33 @@ -1639,7 +1639,7 @@ pyvizio==0.1.61 pyvolumio==0.1.5 # homeassistant.components.waze_travel_time -pywaze==0.4.0 +pywaze==0.5.0 # homeassistant.components.html5 pywebpush==1.9.2 @@ -1684,13 +1684,13 @@ renault-api==0.2.0 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.9 +reolink-aio==0.7.10 # homeassistant.components.rflink rflink==0.0.65 # homeassistant.components.ring -ring-doorbell==0.7.2 +ring-doorbell==0.7.3 # homeassistant.components.roku rokuecp==0.18.1 @@ -1735,7 +1735,7 @@ sense-energy==0.12.1 sense_energy==0.12.1 # homeassistant.components.sensirion_ble -sensirion-ble==0.1.0 +sensirion-ble==0.1.1 # homeassistant.components.sensorpro sensorpro-ble==0.5.3 @@ -1909,7 +1909,7 @@ ultraheat-api==0.5.7 unifi-discovery==1.1.7 # homeassistant.components.zha -universal-silabs-flasher==0.0.13 +universal-silabs-flasher==0.0.14 # homeassistant.components.upb upb-lib==0.5.4 @@ -2015,16 +2015,16 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.2.3 +yalexs-ble==2.3.0 # homeassistant.components.august -yalexs==1.8.0 +yalexs==1.9.0 # homeassistant.components.yeelight yeelight==0.7.13 # homeassistant.components.yolink -yolink-api==0.3.0 +yolink-api==0.3.1 # homeassistant.components.youless youless-api==1.0.1 @@ -2060,7 +2060,7 @@ zigpy-znp==0.11.4 zigpy==0.57.1 # homeassistant.components.zwave_js -zwave-js-server-python==0.51.2 +zwave-js-server-python==0.51.3 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 9323b8e86c0..acdea23444d 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -366,15 +366,19 @@ def _sort_manifest_keys(key: str) -> str: return _SORT_KEYS.get(key, key) -def sort_manifest(integration: Integration) -> bool: +def sort_manifest(integration: Integration, config: Config) -> bool: """Sort manifest.""" keys = list(integration.manifest.keys()) if (keys_sorted := sorted(keys, key=_sort_manifest_keys)) != keys: manifest = {key: integration.manifest[key] for key in keys_sorted} - integration.manifest_path.write_text(json.dumps(manifest, indent=2)) + if config.action == "generate": + integration.manifest_path.write_text(json.dumps(manifest, indent=2)) + text = "have been sorted" + else: + text = "are not sorted correctly" integration.add_error( "manifest", - "Manifest keys have been sorted: domain, name, then alphabetical order", + f"Manifest keys {text}: domain, name, then alphabetical order", ) return True return False @@ -387,9 +391,9 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: for integration in integrations.values(): validate_manifest(integration, core_components_dir) if not integration.errors: - if sort_manifest(integration): + if sort_manifest(integration, config): manifests_resorted.append(integration.manifest_path) - if manifests_resorted: + if config.action == "generate" and manifests_resorted: subprocess.run( ["pre-commit", "run", "--hook-stage", "manual", "prettier", "--files"] + manifests_resorted, diff --git a/script/hassfest/model.py b/script/hassfest/model.py index e4f93c80e81..7df65b8221e 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass, field import json import pathlib -from typing import Any +from typing import Any, Literal @dataclass @@ -26,7 +26,7 @@ class Config: specific_integrations: list[pathlib.Path] | None root: pathlib.Path - action: str + action: Literal["validate", "generate"] requirements: bool errors: list[Error] = field(default_factory=list) cache: dict[str, Any] = field(default_factory=dict) diff --git a/tests/components/airthings_ble/test_sensor.py b/tests/components/airthings_ble/test_sensor.py index 68efd4d25f6..1bf036b735d 100644 --- a/tests/components/airthings_ble/test_sensor.py +++ b/tests/components/airthings_ble/test_sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.airthings_ble.const import DOMAIN +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from tests.components.airthings_ble import ( @@ -31,11 +32,13 @@ async def test_migration_from_v1_to_v3_unique_id(hass: HomeAssistant): assert entry is not None assert device is not None + new_unique_id = f"{WAVE_DEVICE_INFO.address}_temperature" + entity_registry = hass.helpers.entity_registry.async_get(hass) sensor = entity_registry.async_get_or_create( domain=DOMAIN, - platform="sensor", + platform=Platform.SENSOR, unique_id=TEMPERATURE_V1.unique_id, config_entry=entry, device_id=device.id, @@ -57,10 +60,7 @@ async def test_migration_from_v1_to_v3_unique_id(hass: HomeAssistant): assert len(hass.states.async_all()) > 0 - assert ( - entity_registry.async_get(sensor.entity_id).unique_id - == WAVE_DEVICE_INFO.address + "_temperature" - ) + assert entity_registry.async_get(sensor.entity_id).unique_id == new_unique_id async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant): @@ -77,7 +77,7 @@ async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant): sensor = entity_registry.async_get_or_create( domain=DOMAIN, - platform="sensor", + platform=Platform.SENSOR, unique_id=HUMIDITY_V2.unique_id, config_entry=entry, device_id=device.id, @@ -99,10 +99,9 @@ async def test_migration_from_v2_to_v3_unique_id(hass: HomeAssistant): assert len(hass.states.async_all()) > 0 - assert ( - entity_registry.async_get(sensor.entity_id).unique_id - == WAVE_DEVICE_INFO.address + "_humidity" - ) + # Migration should happen, v2 unique id should be updated to the new format + new_unique_id = f"{WAVE_DEVICE_INFO.address}_humidity" + assert entity_registry.async_get(sensor.entity_id).unique_id == new_unique_id async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant): @@ -119,7 +118,7 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant): v2 = entity_registry.async_get_or_create( domain=DOMAIN, - platform="sensor", + platform=Platform.SENSOR, unique_id=CO2_V2.unique_id, config_entry=entry, device_id=device.id, @@ -127,7 +126,7 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant): v1 = entity_registry.async_get_or_create( domain=DOMAIN, - platform="sensor", + platform=Platform.SENSOR, unique_id=CO2_V1.unique_id, config_entry=entry, device_id=device.id, @@ -149,11 +148,10 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id(hass: HomeAssistant): assert len(hass.states.async_all()) > 0 - assert ( - entity_registry.async_get(v1.entity_id).unique_id - == WAVE_DEVICE_INFO.address + "_co2" - ) - assert entity_registry.async_get(v2.entity_id).unique_id == v2.unique_id + # Migration should happen, v1 unique id should be updated to the new format + new_unique_id = f"{WAVE_DEVICE_INFO.address}_co2" + assert entity_registry.async_get(v1.entity_id).unique_id == new_unique_id + assert entity_registry.async_get(v2.entity_id).unique_id == CO2_V2.unique_id async def test_migration_with_all_unique_ids(hass: HomeAssistant): @@ -170,7 +168,7 @@ async def test_migration_with_all_unique_ids(hass: HomeAssistant): v1 = entity_registry.async_get_or_create( domain=DOMAIN, - platform="sensor", + platform=Platform.SENSOR, unique_id=VOC_V1.unique_id, config_entry=entry, device_id=device.id, @@ -178,7 +176,7 @@ async def test_migration_with_all_unique_ids(hass: HomeAssistant): v2 = entity_registry.async_get_or_create( domain=DOMAIN, - platform="sensor", + platform=Platform.SENSOR, unique_id=VOC_V2.unique_id, config_entry=entry, device_id=device.id, @@ -186,7 +184,7 @@ async def test_migration_with_all_unique_ids(hass: HomeAssistant): v3 = entity_registry.async_get_or_create( domain=DOMAIN, - platform="sensor", + platform=Platform.SENSOR, unique_id=VOC_V3.unique_id, config_entry=entry, device_id=device.id, @@ -208,6 +206,7 @@ async def test_migration_with_all_unique_ids(hass: HomeAssistant): assert len(hass.states.async_all()) > 0 - assert entity_registry.async_get(v1.entity_id).unique_id == v1.unique_id - assert entity_registry.async_get(v2.entity_id).unique_id == v2.unique_id - assert entity_registry.async_get(v3.entity_id).unique_id == v3.unique_id + # No migration should happen, unique id should be the same as before + assert entity_registry.async_get(v1.entity_id).unique_id == VOC_V1.unique_id + assert entity_registry.async_get(v2.entity_id).unique_id == VOC_V2.unique_id + assert entity_registry.async_get(v3.entity_id).unique_id == VOC_V3.unique_id diff --git a/tests/components/comelit/test_config_flow.py b/tests/components/comelit/test_config_flow.py index 2fb9e836efb..10f68f4d7c1 100644 --- a/tests/components/comelit/test_config_flow.py +++ b/tests/components/comelit/test_config_flow.py @@ -18,9 +18,9 @@ from tests.common import MockConfigEntry async def test_user(hass: HomeAssistant) -> None: """Test starting a flow by user.""" with patch( - "aiocomelit.api.ComeliteSerialBridgeAPi.login", + "aiocomelit.api.ComeliteSerialBridgeApi.login", ), patch( - "aiocomelit.api.ComeliteSerialBridgeAPi.logout", + "aiocomelit.api.ComeliteSerialBridgeApi.logout", ), patch( "homeassistant.components.comelit.async_setup_entry" ) as mock_setup_entry, patch( @@ -64,7 +64,7 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> assert result["step_id"] == "user" with patch( - "aiocomelit.api.ComeliteSerialBridgeAPi.login", + "aiocomelit.api.ComeliteSerialBridgeApi.login", side_effect=side_effect, ): result = await hass.config_entries.flow.async_configure( @@ -83,9 +83,9 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: mock_config.add_to_hass(hass) with patch( - "aiocomelit.api.ComeliteSerialBridgeAPi.login", + "aiocomelit.api.ComeliteSerialBridgeApi.login", ), patch( - "aiocomelit.api.ComeliteSerialBridgeAPi.logout", + "aiocomelit.api.ComeliteSerialBridgeApi.logout", ), patch("homeassistant.components.comelit.async_setup_entry"), patch( "requests.get" ) as mock_request_get: @@ -127,9 +127,9 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> mock_config.add_to_hass(hass) with patch( - "aiocomelit.api.ComeliteSerialBridgeAPi.login", side_effect=side_effect + "aiocomelit.api.ComeliteSerialBridgeApi.login", side_effect=side_effect ), patch( - "aiocomelit.api.ComeliteSerialBridgeAPi.logout", + "aiocomelit.api.ComeliteSerialBridgeApi.logout", ), patch( "homeassistant.components.comelit.async_setup_entry" ): diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 08def9a923e..133f38c1a56 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -198,6 +198,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, State from .test_common import ( + help_custom_config, help_test_availability_when_connection_lost, help_test_availability_without_topic, help_test_custom_availability_payload, @@ -441,6 +442,176 @@ async def test_controlling_state_via_topic( assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + light.DOMAIN, + DEFAULT_CONFIG, + ( + { + "state_topic": "test-topic", + "optimistic": True, + "brightness_command_topic": "test_light_rgb/brightness/set", + "color_mode_state_topic": "color-mode-state-topic", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgb_state_topic": "rgb-state-topic", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbw_state_topic": "rgbw-state-topic", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "rgbww_state_topic": "rgbww-state-topic", + }, + ), + ) + ], +) +async def test_received_rgbx_values_set_state_optimistic( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the state is set correctly when an rgbx update is received.""" + await mqtt_mock_entry() + state = hass.states.get("light.test") + assert state and state.state is not None + async_fire_mqtt_message(hass, "test-topic", "ON") + ## Test rgb processing + async_fire_mqtt_message(hass, "rgb-state-topic", "255,255,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgb" + assert state.attributes["rgb_color"] == (255, 255, 255) + + # Only update color mode + async_fire_mqtt_message(hass, "color-mode-state-topic", "rgbww") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbww" + + # Resending same rgb value should restore color mode + async_fire_mqtt_message(hass, "rgb-state-topic", "255,255,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgb" + assert state.attributes["rgb_color"] == (255, 255, 255) + + # Only update brightness + await common.async_turn_on(hass, "light.test", brightness=128) + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 128 + assert state.attributes["color_mode"] == "rgb" + assert state.attributes["rgb_color"] == (255, 255, 255) + + # Resending same rgb value should restore brightness + async_fire_mqtt_message(hass, "rgb-state-topic", "255,255,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgb" + assert state.attributes["rgb_color"] == (255, 255, 255) + + # Only change rgb value + async_fire_mqtt_message(hass, "rgb-state-topic", "255,255,0") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgb" + assert state.attributes["rgb_color"] == (255, 255, 0) + + ## Test rgbw processing + async_fire_mqtt_message(hass, "rgbw-state-topic", "255,255,255,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbw" + assert state.attributes["rgbw_color"] == (255, 255, 255, 255) + + # Only update color mode + async_fire_mqtt_message(hass, "color-mode-state-topic", "rgb") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgb" + + # Resending same rgbw value should restore color mode + async_fire_mqtt_message(hass, "rgbw-state-topic", "255,255,255,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbw" + assert state.attributes["rgbw_color"] == (255, 255, 255, 255) + + # Only update brightness + await common.async_turn_on(hass, "light.test", brightness=128) + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 128 + assert state.attributes["color_mode"] == "rgbw" + assert state.attributes["rgbw_color"] == (255, 255, 255, 255) + + # Resending same rgbw value should restore brightness + async_fire_mqtt_message(hass, "rgbw-state-topic", "255,255,255,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbw" + assert state.attributes["rgbw_color"] == (255, 255, 255, 255) + + # Only change rgbw value + async_fire_mqtt_message(hass, "rgbw-state-topic", "255,255,128,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbw" + assert state.attributes["rgbw_color"] == (255, 255, 128, 255) + + ## Test rgbww processing + async_fire_mqtt_message(hass, "rgbww-state-topic", "255,255,255,32,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbww" + assert state.attributes["rgbww_color"] == (255, 255, 255, 32, 255) + + # Only update color mode + async_fire_mqtt_message(hass, "color-mode-state-topic", "rgb") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgb" + + # Resending same rgbw value should restore color mode + async_fire_mqtt_message(hass, "rgbww-state-topic", "255,255,255,32,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbww" + assert state.attributes["rgbww_color"] == (255, 255, 255, 32, 255) + + # Only update brightness + await common.async_turn_on(hass, "light.test", brightness=128) + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 128 + assert state.attributes["color_mode"] == "rgbww" + assert state.attributes["rgbww_color"] == (255, 255, 255, 32, 255) + + # Resending same rgbww value should restore brightness + async_fire_mqtt_message(hass, "rgbww-state-topic", "255,255,255,32,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbww" + assert state.attributes["rgbww_color"] == (255, 255, 255, 32, 255) + + # Only change rgbww value + async_fire_mqtt_message(hass, "rgbww-state-topic", "255,255,128,32,255") + await hass.async_block_till_done() + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 255 + assert state.attributes["color_mode"] == "rgbww" + assert state.attributes["rgbww_color"] == (255, 255, 128, 32, 255) + + @pytest.mark.parametrize( "hass_config", [ diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index 248ad3a69ea..37787024fb6 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -76,41 +76,6 @@ def mock_controller_service(): yield service_mock -@pytest.fixture(name="service_5555") -def mock_controller_service_5555(): - """Mock a successful service.""" - with patch( - "homeassistant.components.netgear.async_setup_entry", return_value=True - ), patch("homeassistant.components.netgear.router.Netgear") as service_mock: - service_mock.return_value.get_info = Mock(return_value=ROUTER_INFOS) - service_mock.return_value.port = 5555 - service_mock.return_value.ssl = True - yield service_mock - - -@pytest.fixture(name="service_incomplete") -def mock_controller_service_incomplete(): - """Mock a successful service.""" - router_infos = ROUTER_INFOS.copy() - router_infos.pop("DeviceName") - with patch( - "homeassistant.components.netgear.async_setup_entry", return_value=True - ), patch("homeassistant.components.netgear.router.Netgear") as service_mock: - service_mock.return_value.get_info = Mock(return_value=router_infos) - service_mock.return_value.port = 80 - service_mock.return_value.ssl = False - yield service_mock - - -@pytest.fixture(name="service_failed") -def mock_controller_service_failed(): - """Mock a failed service.""" - with patch("homeassistant.components.netgear.router.Netgear") as service_mock: - service_mock.return_value.login_try_port = Mock(return_value=None) - service_mock.return_value.get_info = Mock(return_value=None) - yield service_mock - - async def test_user(hass: HomeAssistant, service) -> None: """Test user step.""" result = await hass.config_entries.flow.async_init( @@ -138,7 +103,7 @@ async def test_user(hass: HomeAssistant, service) -> None: assert result["data"][CONF_PASSWORD] == PASSWORD -async def test_user_connect_error(hass: HomeAssistant, service_failed) -> None: +async def test_user_connect_error(hass: HomeAssistant, service) -> None: """Test user step with connection failure.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -146,7 +111,23 @@ async def test_user_connect_error(hass: HomeAssistant, service_failed) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" + service.return_value.get_info = Mock(return_value=None) + # Have to provide all config + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "info"} + + service.return_value.login_try_port = Mock(return_value=None) + result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -160,7 +141,7 @@ async def test_user_connect_error(hass: HomeAssistant, service_failed) -> None: assert result["errors"] == {"base": "config"} -async def test_user_incomplete_info(hass: HomeAssistant, service_incomplete) -> None: +async def test_user_incomplete_info(hass: HomeAssistant, service) -> None: """Test user step with incomplete device info.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -168,6 +149,10 @@ async def test_user_incomplete_info(hass: HomeAssistant, service_incomplete) -> assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" + router_infos = ROUTER_INFOS.copy() + router_infos.pop("DeviceName") + service.return_value.get_info = Mock(return_value=router_infos) + # Have to provide all config result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -313,7 +298,7 @@ async def test_ssdp(hass: HomeAssistant, service) -> None: assert result["data"][CONF_PASSWORD] == PASSWORD -async def test_ssdp_port_5555(hass: HomeAssistant, service_5555) -> None: +async def test_ssdp_port_5555(hass: HomeAssistant, service) -> None: """Test ssdp step with port 5555.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -332,6 +317,9 @@ async def test_ssdp_port_5555(hass: HomeAssistant, service_5555) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" + service.return_value.port = 5555 + service.return_value.ssl = True + result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD} ) diff --git a/tests/components/rainbird/conftest.py b/tests/components/rainbird/conftest.py index 9e4e4e546cb..40b400210aa 100644 --- a/tests/components/rainbird/conftest.py +++ b/tests/components/rainbird/conftest.py @@ -35,6 +35,7 @@ SERIAL_NUMBER = 0x12635436566 # Get serial number Command 0x85. Serial is 0x12635436566 SERIAL_RESPONSE = "850000012635436566" +ZERO_SERIAL_RESPONSE = "850000000000000000" # Model and version command 0x82 MODEL_AND_VERSION_RESPONSE = "820006090C" # Get available stations command 0x83 @@ -84,6 +85,12 @@ def yaml_config() -> dict[str, Any]: return {} +@pytest.fixture +async def unique_id() -> str: + """Fixture for serial number used in the config entry.""" + return SERIAL_NUMBER + + @pytest.fixture async def config_entry_data() -> dict[str, Any]: """Fixture for MockConfigEntry data.""" @@ -92,13 +99,14 @@ async def config_entry_data() -> dict[str, Any]: @pytest.fixture async def config_entry( - config_entry_data: dict[str, Any] | None + config_entry_data: dict[str, Any] | None, + unique_id: str, ) -> MockConfigEntry | None: """Fixture for MockConfigEntry.""" if config_entry_data is None: return None return MockConfigEntry( - unique_id=SERIAL_NUMBER, + unique_id=unique_id, domain=DOMAIN, data=config_entry_data, options={ATTR_DURATION: DEFAULT_TRIGGER_TIME_MINUTES}, diff --git a/tests/components/rainbird/test_config_flow.py b/tests/components/rainbird/test_config_flow.py index f11eba4fed7..e7337ad6508 100644 --- a/tests/components/rainbird/test_config_flow.py +++ b/tests/components/rainbird/test_config_flow.py @@ -3,6 +3,7 @@ import asyncio from collections.abc import Generator from http import HTTPStatus +from typing import Any from unittest.mock import Mock, patch import pytest @@ -19,8 +20,11 @@ from .conftest import ( CONFIG_ENTRY_DATA, HOST, PASSWORD, + SERIAL_NUMBER, SERIAL_RESPONSE, URL, + ZERO_SERIAL_RESPONSE, + ComponentSetup, mock_response, ) @@ -66,19 +70,132 @@ async def complete_flow(hass: HomeAssistant) -> FlowResult: ) -async def test_controller_flow(hass: HomeAssistant, mock_setup: Mock) -> None: +@pytest.mark.parametrize( + ("responses", "expected_config_entry", "expected_unique_id"), + [ + ( + [mock_response(SERIAL_RESPONSE)], + CONFIG_ENTRY_DATA, + SERIAL_NUMBER, + ), + ( + [mock_response(ZERO_SERIAL_RESPONSE)], + {**CONFIG_ENTRY_DATA, "serial_number": 0}, + None, + ), + ], +) +async def test_controller_flow( + hass: HomeAssistant, + mock_setup: Mock, + expected_config_entry: dict[str, str], + expected_unique_id: int | None, +) -> None: """Test the controller is setup correctly.""" result = await complete_flow(hass) assert result.get("type") == "create_entry" assert result.get("title") == HOST assert "result" in result - assert result["result"].data == CONFIG_ENTRY_DATA + assert dict(result["result"].data) == expected_config_entry assert result["result"].options == {ATTR_DURATION: 6} + assert result["result"].unique_id == expected_unique_id assert len(mock_setup.mock_calls) == 1 +@pytest.mark.parametrize( + ( + "unique_id", + "config_entry_data", + "config_flow_responses", + "expected_config_entry", + ), + [ + ( + "other-serial-number", + {**CONFIG_ENTRY_DATA, "host": "other-host"}, + [mock_response(SERIAL_RESPONSE)], + CONFIG_ENTRY_DATA, + ), + ( + None, + {**CONFIG_ENTRY_DATA, "serial_number": 0, "host": "other-host"}, + [mock_response(ZERO_SERIAL_RESPONSE)], + {**CONFIG_ENTRY_DATA, "serial_number": 0}, + ), + ], + ids=["with-serial", "zero-serial"], +) +async def test_multiple_config_entries( + hass: HomeAssistant, + setup_integration: ComponentSetup, + responses: list[AiohttpClientMockResponse], + config_flow_responses: list[AiohttpClientMockResponse], + expected_config_entry: dict[str, Any] | None, +) -> None: + """Test setting up multiple config entries that refer to different devices.""" + assert await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + responses.clear() + responses.extend(config_flow_responses) + + result = await complete_flow(hass) + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert dict(result.get("result").data) == expected_config_entry + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 2 + + +@pytest.mark.parametrize( + ( + "unique_id", + "config_entry_data", + "config_flow_responses", + ), + [ + ( + SERIAL_NUMBER, + CONFIG_ENTRY_DATA, + [mock_response(SERIAL_RESPONSE)], + ), + ( + None, + {**CONFIG_ENTRY_DATA, "serial_number": 0}, + [mock_response(ZERO_SERIAL_RESPONSE)], + ), + ], + ids=[ + "duplicate-serial-number", + "duplicate-host-port-no-serial", + ], +) +async def test_duplicate_config_entries( + hass: HomeAssistant, + setup_integration: ComponentSetup, + responses: list[AiohttpClientMockResponse], + config_flow_responses: list[AiohttpClientMockResponse], +) -> None: + """Test that a device can not be registered twice.""" + assert await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + responses.clear() + responses.extend(config_flow_responses) + + result = await complete_flow(hass) + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "already_configured" + + async def test_controller_cannot_connect( hass: HomeAssistant, mock_setup: Mock, diff --git a/tests/components/roborock/snapshots/test_diagnostics.ambr b/tests/components/roborock/snapshots/test_diagnostics.ambr index eb70e04110f..a766a6c2703 100644 --- a/tests/components/roborock/snapshots/test_diagnostics.ambr +++ b/tests/components/roborock/snapshots/test_diagnostics.ambr @@ -225,11 +225,13 @@ 'area': 20965000, 'avoidCount': 19, 'begin': 1672543330, + 'beginDatetime': '2023-01-01T03:22:10+00:00', 'cleanType': 3, 'complete': 1, 'duration': 1176, 'dustCollectionStatus': 1, 'end': 1672544638, + 'endDatetime': '2023-01-01T03:43:58+00:00', 'error': 0, 'finishReason': 56, 'mapFlag': 0, diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 1f836ad9095..6ca26433056 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -44,6 +44,7 @@ from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from tests.common import ( MockConfigEntry, + MockEntityPlatform, MockModule, MockPlatform, async_mock_restore_state_shutdown_restart, @@ -2177,27 +2178,24 @@ async def test_unit_conversion_update( entity_registry = er.async_get(hass) platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = platform.MockSensor( name="Test 0", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] - platform.ENTITIES["1"] = platform.MockSensor( + entity1 = platform.MockSensor( name="Test 1", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), unique_id="very_unique_1", ) - entity1 = platform.ENTITIES["1"] - platform.ENTITIES["2"] = platform.MockSensor( + entity2 = platform.MockSensor( name="Test 2", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2205,9 +2203,8 @@ async def test_unit_conversion_update( suggested_unit_of_measurement=suggested_unit, unique_id="very_unique_2", ) - entity2 = platform.ENTITIES["2"] - platform.ENTITIES["3"] = platform.MockSensor( + entity3 = platform.MockSensor( name="Test 3", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2215,9 +2212,33 @@ async def test_unit_conversion_update( suggested_unit_of_measurement=suggested_unit, unique_id="very_unique_3", ) - entity3 = platform.ENTITIES["3"] - assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + entity4 = platform.MockSensor( + name="Test 4", + device_class=device_class, + native_unit_of_measurement=native_unit, + native_value=str(native_value), + unique_id="very_unique_4", + ) + + entity_platform = MockEntityPlatform( + hass, domain="sensor", platform_name="test", platform=None + ) + await entity_platform.async_add_entities((entity0, entity1, entity2, entity3)) + + # Pre-register entity4 + entry = entity_registry.async_get_or_create( + "sensor", "test", entity4.unique_id, unit_of_measurement=automatic_unit_1 + ) + entity4_entity_id = entry.entity_id + entity_registry.async_update_entity_options( + entity4_entity_id, + "sensor.private", + { + "suggested_unit_of_measurement": automatic_unit_1, + }, + ) + await hass.async_block_till_done() # Registered entity -> Follow automatic unit conversion @@ -2320,6 +2341,25 @@ async def test_unit_conversion_update( assert state.state == suggested_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit + # Entity 4 still has a pending request to refresh entity options + entry = entity_registry.async_get(entity4_entity_id) + assert entry.options == { + "sensor.private": { + "refresh_initial_entity_options": True, + "suggested_unit_of_measurement": automatic_unit_1, + } + } + + # Add entity 4, the pending request to refresh entity options should be handled + await entity_platform.async_add_entities((entity4,)) + + state = hass.states.get(entity4_entity_id) + assert state.state == automatic_state_2 + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == automatic_unit_2 + + entry = entity_registry.async_get(entity4_entity_id) + assert entry.options == {} + class MockFlow(ConfigFlow): """Test flow.""" diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index b8f197a4dee..43d68d87362 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -1460,6 +1460,39 @@ def test_calculate_adjustment_invalid_new_state( assert "Invalid state unknown" in caplog.text +async def test_unit_of_measurement_missing_invalid_new_state( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that a suggestion is created when new_state is missing unit_of_measurement.""" + yaml_config = { + "utility_meter": { + "energy_bill": { + "source": "sensor.energy", + } + } + } + source_entity_id = yaml_config[DOMAIN]["energy_bill"]["source"] + + assert await async_setup_component(hass, DOMAIN, yaml_config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.states.async_set(source_entity_id, 4, {ATTR_UNIT_OF_MEASUREMENT: None}) + + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_bill") + assert state is not None + assert state.state == "0" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert ( + f"Source sensor {source_entity_id} has no unit of measurement." in caplog.text + ) + + async def test_device_id(hass: HomeAssistant) -> None: """Test for source entity device for Utility Meter.""" device_registry = dr.async_get(hass)