diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 9d13c07301e..2fdde85c8cd 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.09.0 + uses: home-assistant/builder@2023.12.0 with: args: | $BUILD_ARGS \ @@ -247,6 +247,7 @@ jobs: - raspberrypi3-64 - raspberrypi4 - raspberrypi4-64 + - raspberrypi5-64 - tinker - yellow - green @@ -273,7 +274,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2023.09.0 + uses: home-assistant/builder@2023.12.0 with: args: | $BUILD_ARGS \ diff --git a/homeassistant/components/acmeda/manifest.json b/homeassistant/components/acmeda/manifest.json index 94dcf3325ca..a8b3c7c829f 100644 --- a/homeassistant/components/acmeda/manifest.json +++ b/homeassistant/components/acmeda/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/acmeda", "iot_class": "local_push", "loggers": ["aiopulse"], - "requirements": ["aiopulse==0.4.3"] + "requirements": ["aiopulse==0.4.4"] } diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index 22172255b9b..f5a0e1b109e 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -248,7 +248,6 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): self._attr_hvac_mode = HVACMode.OFF self._attr_max_temp = self.get_airzone_value(AZD_TEMP_MAX) self._attr_min_temp = self.get_airzone_value(AZD_TEMP_MIN) - self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET) if self.supported_features & ClimateEntityFeature.FAN_MODE: self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED)) if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE: @@ -258,3 +257,5 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): self._attr_target_temperature_low = self.get_airzone_value( AZD_HEAT_TEMP_SET ) + else: + self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index e9485f1b9d0..20b8a452324 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==0.6.9"] + "requirements": ["aioairzone==0.7.2"] } diff --git a/homeassistant/components/blink/diagnostics.py b/homeassistant/components/blink/diagnostics.py index f69c1721bf1..664d1421ac2 100644 --- a/homeassistant/components/blink/diagnostics.py +++ b/homeassistant/components/blink/diagnostics.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN -TO_REDACT = {"serial", "macaddress", "username", "password", "token"} +TO_REDACT = {"serial", "macaddress", "username", "password", "token", "unique_id"} async def async_get_config_entry_diagnostics( diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index db3ab91de11..a1268919052 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/blink", "iot_class": "cloud_polling", "loggers": ["blinkpy"], - "requirements": ["blinkpy==0.22.3"] + "requirements": ["blinkpy==0.22.4"] } diff --git a/homeassistant/components/devialet/manifest.json b/homeassistant/components/devialet/manifest.json index 286b9bfb112..dd30f91c835 100644 --- a/homeassistant/components/devialet/manifest.json +++ b/homeassistant/components/devialet/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/devialet", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["devialet==1.4.3"], + "requirements": ["devialet==1.4.5"], "zeroconf": ["_devialet-http._tcp.local."] } diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index c49e1f143e6..4ae7760a56b 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.14.3"], + "requirements": ["pyenphase==1.15.2"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/fritz/image.py b/homeassistant/components/fritz/image.py index d14c562bd76..aa1ede5a185 100644 --- a/homeassistant/components/fritz/image.py +++ b/homeassistant/components/fritz/image.py @@ -5,6 +5,8 @@ from __future__ import annotations from io import BytesIO import logging +from requests.exceptions import RequestException + from homeassistant.components.image import ImageEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory @@ -78,7 +80,13 @@ class FritzGuestWifiQRImage(FritzBoxBaseEntity, ImageEntity): async def async_update(self) -> None: """Update the image entity data.""" - qr_bytes = await self._fetch_image() + try: + qr_bytes = await self._fetch_image() + except RequestException: + self._current_qr_bytes = None + self._attr_image_last_updated = None + self.async_write_ha_state() + return if self._current_qr_bytes != qr_bytes: dt_now = dt_util.utcnow() diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index e766a53518a..e36056d2fab 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -85,7 +85,7 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities(set(coordinator.data.devices.keys())) + _add_entities(set(coordinator.data.devices)) class FritzboxBinarySensor(FritzBoxDeviceEntity, BinarySensorEntity): diff --git a/homeassistant/components/fritzbox/button.py b/homeassistant/components/fritzbox/button.py index e56ebc1e3b0..6695c564331 100644 --- a/homeassistant/components/fritzbox/button.py +++ b/homeassistant/components/fritzbox/button.py @@ -29,7 +29,7 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities(set(coordinator.data.templates.keys())) + _add_entities(set(coordinator.data.templates)) class FritzBoxTemplate(FritzBoxEntity, ButtonEntity): diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 6ce885a3fdb..f648d4b3966 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -66,7 +66,7 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities(set(coordinator.data.devices.keys())) + _add_entities(set(coordinator.data.devices)) class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): diff --git a/homeassistant/components/fritzbox/cover.py b/homeassistant/components/fritzbox/cover.py index d3d4c9080ea..4c2ba76c377 100644 --- a/homeassistant/components/fritzbox/cover.py +++ b/homeassistant/components/fritzbox/cover.py @@ -38,7 +38,7 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities(set(coordinator.data.devices.keys())) + _add_entities(set(coordinator.data.devices)) class FritzboxCover(FritzBoxDeviceEntity, CoverEntity): diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py index 88d32fe33a5..cb0c8594695 100644 --- a/homeassistant/components/fritzbox/light.py +++ b/homeassistant/components/fritzbox/light.py @@ -44,7 +44,7 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities(set(coordinator.data.devices.keys())) + _add_entities(set(coordinator.data.devices)) class FritzboxLight(FritzBoxDeviceEntity, LightEntity): diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index fda8b239859..140ecaef331 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -230,7 +230,7 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities(set(coordinator.data.devices.keys())) + _add_entities(set(coordinator.data.devices)) class FritzBoxSensor(FritzBoxDeviceEntity, SensorEntity): diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 4a2960a18ea..4d93cddb617 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -33,7 +33,7 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(_add_entities)) - _add_entities(set(coordinator.data.devices.keys())) + _add_entities(set(coordinator.data.devices)) class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity): diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index e7ab7aac3c8..3dd9b11ae64 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -270,6 +270,7 @@ HARDWARE_INTEGRATIONS = { "rpi3-64": "raspberry_pi", "rpi4": "raspberry_pi", "rpi4-64": "raspberry_pi", + "rpi5-64": "raspberry_pi", "yellow": "homeassistant_yellow", } diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index dfac69b3aed..f76c78d52d2 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -507,6 +507,7 @@ class HoneywellUSThermostat(ClimateEntity): except ( AuthError, ClientConnectionError, + AscConnectionError, asyncio.TimeoutError, ): self._retry += 1 diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index 18b83013d70..481d006809d 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/life360", "iot_class": "cloud_polling", "loggers": ["life360"], - "requirements": ["life360==6.0.0"] + "requirements": ["life360==6.0.1"] } diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index cc31ff42edf..f9115cd8146 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -21,5 +21,5 @@ "documentation": "https://www.home-assistant.io/integrations/motion_blinds", "iot_class": "local_push", "loggers": ["motionblinds"], - "requirements": ["motionblinds==0.6.18"] + "requirements": ["motionblinds==0.6.19"] } diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index e3bd8952b55..b796372fc20 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -186,11 +186,6 @@ class NetatmoLight(NetatmoBase, LightEntity): ] ) - @property - def is_on(self) -> bool: - """Return true if light is on.""" - return self._dimmer.on is True - async def async_turn_on(self, **kwargs: Any) -> None: """Turn light on.""" if ATTR_BRIGHTNESS in kwargs: @@ -211,6 +206,8 @@ class NetatmoLight(NetatmoBase, LightEntity): @callback def async_update_callback(self) -> None: """Update the entity's state.""" + self._attr_is_on = self._dimmer.on is True + if self._dimmer.brightness is not None: # Netatmo uses a range of [0, 100] to control brightness self._attr_brightness = round((self._dimmer.brightness / 100) * 255) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 7d84641874a..f5f2d67947f 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["pyatmo"], - "requirements": ["pyatmo==7.6.0"] + "requirements": ["pyatmo==8.0.1"] } diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 10114a75f63..2f99b866cf2 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -447,17 +447,16 @@ class NetatmoWeatherSensor(NetatmoBase, SensorEntity): } ) - @property - def available(self) -> bool: - """Return entity availability.""" - return self.state is not None - @callback def async_update_callback(self) -> None: """Update the entity's state.""" if ( - state := getattr(self._module, self.entity_description.netatmo_name) - ) is None: + not self._module.reachable + or (state := getattr(self._module, self.entity_description.netatmo_name)) + is None + ): + if self.available: + self._attr_available = False return if self.entity_description.netatmo_name in { @@ -475,6 +474,7 @@ class NetatmoWeatherSensor(NetatmoBase, SensorEntity): else: self._attr_native_value = state + self._attr_available = True self.async_write_ha_state() @@ -519,7 +519,6 @@ class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity): if not self._module.reachable: if self.available: self._attr_available = False - self._attr_native_value = None return self._attr_available = True @@ -565,9 +564,15 @@ class NetatmoSensor(NetatmoBase, SensorEntity): @callback def async_update_callback(self) -> None: """Update the entity's state.""" + if not self._module.reachable: + if self.available: + self._attr_available = False + return + if (state := getattr(self._module, self.entity_description.key)) is None: return + self._attr_available = True self._attr_native_value = state self.async_write_ha_state() @@ -777,7 +782,6 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): self.entity_description.key, self._area_name, ) - self._attr_native_value = None self._attr_available = False return diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 036ef6e4f0e..406acd6aabd 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -290,17 +290,19 @@ class NotionEntity(CoordinatorEntity[DataUpdateCoordinator[NotionData]]): """Initialize the entity.""" super().__init__(coordinator) - bridge = self.coordinator.data.bridges[bridge_id] sensor = self.coordinator.data.sensors[sensor_id] + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, sensor.hardware_id)}, manufacturer="Silicon Labs", model=str(sensor.hardware_revision), name=str(sensor.name).capitalize(), sw_version=sensor.firmware_version, - via_device=(DOMAIN, bridge.hardware_id), ) + if bridge := self._async_get_bridge(bridge_id): + self._attr_device_info["via_device"] = (DOMAIN, bridge.hardware_id) + self._attr_extra_state_attributes = {} self._attr_unique_id = listener_id self._bridge_id = bridge_id @@ -322,6 +324,14 @@ class NotionEntity(CoordinatorEntity[DataUpdateCoordinator[NotionData]]): """Return the listener related to this entity.""" return self.coordinator.data.listeners[self._listener_id] + @callback + def _async_get_bridge(self, bridge_id: int) -> Bridge | None: + """Get a bridge by ID (if it exists).""" + if (bridge := self.coordinator.data.bridges.get(bridge_id)) is None: + LOGGER.debug("Entity references a non-existent bridge ID: %s", bridge_id) + return None + return bridge + @callback def _async_update_bridge_id(self) -> None: """Update the entity's bridge ID if it has changed. @@ -330,13 +340,12 @@ class NotionEntity(CoordinatorEntity[DataUpdateCoordinator[NotionData]]): """ sensor = self.coordinator.data.sensors[self._sensor_id] - # If the sensor's bridge ID is the same as what we had before or if it points - # to a bridge that doesn't exist (which can happen due to a Notion API bug), - # return immediately: - if ( - self._bridge_id == sensor.bridge.id - or sensor.bridge.id not in self.coordinator.data.bridges - ): + # If the bridge ID hasn't changed, return: + if self._bridge_id == sensor.bridge.id: + return + + # If the bridge doesn't exist, return: + if (bridge := self._async_get_bridge(sensor.bridge.id)) is None: return self._bridge_id = sensor.bridge.id diff --git a/homeassistant/components/ourgroceries/__init__.py b/homeassistant/components/ourgroceries/__init__.py index d645b8617c2..ebb928e72d0 100644 --- a/homeassistant/components/ourgroceries/__init__.py +++ b/homeassistant/components/ourgroceries/__init__.py @@ -24,16 +24,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) data = entry.data og = OurGroceries(data[CONF_USERNAME], data[CONF_PASSWORD]) - lists = [] try: await og.login() - lists = (await og.get_my_lists())["shoppingLists"] except (AsyncIOTimeoutError, ClientError) as error: raise ConfigEntryNotReady from error except InvalidLoginException: return False - coordinator = OurGroceriesDataUpdateCoordinator(hass, og, lists) + coordinator = OurGroceriesDataUpdateCoordinator(hass, og) await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = coordinator diff --git a/homeassistant/components/ourgroceries/coordinator.py b/homeassistant/components/ourgroceries/coordinator.py index 636ebcc300a..c583fb4d5b1 100644 --- a/homeassistant/components/ourgroceries/coordinator.py +++ b/homeassistant/components/ourgroceries/coordinator.py @@ -20,13 +20,11 @@ _LOGGER = logging.getLogger(__name__) class OurGroceriesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): """Class to manage fetching OurGroceries data.""" - def __init__( - self, hass: HomeAssistant, og: OurGroceries, lists: list[dict] - ) -> None: + def __init__(self, hass: HomeAssistant, og: OurGroceries) -> None: """Initialize global OurGroceries data updater.""" self.og = og - self.lists = lists - self._ids = [sl["id"] for sl in lists] + self.lists: list[dict] = [] + self._cache: dict[str, dict] = {} interval = timedelta(seconds=SCAN_INTERVAL) super().__init__( hass, @@ -35,13 +33,16 @@ class OurGroceriesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): update_interval=interval, ) + async def _update_list(self, list_id: str, version_id: str) -> None: + old_version = self._cache.get(list_id, {}).get("list", {}).get("versionId", "") + if old_version == version_id: + return + self._cache[list_id] = await self.og.get_list_items(list_id=list_id) + async def _async_update_data(self) -> dict[str, dict]: """Fetch data from OurGroceries.""" - return dict( - zip( - self._ids, - await asyncio.gather( - *[self.og.get_list_items(list_id=id) for id in self._ids] - ), - ) + self.lists = (await self.og.get_my_lists())["shoppingLists"] + await asyncio.gather( + *[self._update_list(sl["id"], sl["versionId"]) for sl in self.lists] ) + return self._cache diff --git a/homeassistant/components/raspberry_pi/hardware.py b/homeassistant/components/raspberry_pi/hardware.py index e90316ccb3c..2141ff6034d 100644 --- a/homeassistant/components/raspberry_pi/hardware.py +++ b/homeassistant/components/raspberry_pi/hardware.py @@ -17,6 +17,7 @@ BOARD_NAMES = { "rpi3-64": "Raspberry Pi 3", "rpi4": "Raspberry Pi 4 (32-bit)", "rpi4-64": "Raspberry Pi 4", + "rpi5-64": "Raspberry Pi 5", } MODELS = { @@ -28,6 +29,7 @@ MODELS = { "rpi3-64": "3", "rpi4": "4", "rpi4-64": "4", + "rpi5-64": "5", } diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index e03fa28b7ce..e687fc5d9b1 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.8.2"] + "requirements": ["reolink-aio==0.8.4"] } diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 102bb024924..991bfff7da0 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -202,22 +202,22 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity): rendered_headers = template.render_complex(self._headers, parse_result=False) rendered_params = template.render_complex(self._params) - async with asyncio.timeout(self._timeout): - req: httpx.Response = await getattr(websession, self._method)( - self._resource, - auth=self._auth, - content=bytes(body, "utf-8"), - headers=rendered_headers, - params=rendered_params, - ) - return req + req: httpx.Response = await getattr(websession, self._method)( + self._resource, + auth=self._auth, + content=bytes(body, "utf-8"), + headers=rendered_headers, + params=rendered_params, + timeout=self._timeout, + ) + return req async def async_update(self) -> None: """Get the current state, catching errors.""" req = None try: req = await self.get_device_state(self.hass) - except asyncio.TimeoutError: + except (asyncio.TimeoutError, httpx.TimeoutException): _LOGGER.exception("Timed out while fetching data") except httpx.RequestError as err: _LOGGER.exception("Error while fetching data: %s", err) @@ -233,14 +233,14 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity): rendered_headers = template.render_complex(self._headers, parse_result=False) rendered_params = template.render_complex(self._params) - async with asyncio.timeout(self._timeout): - req = await websession.get( - self._state_resource, - auth=self._auth, - headers=rendered_headers, - params=rendered_params, - ) - text = req.text + req = await websession.get( + self._state_resource, + auth=self._auth, + headers=rendered_headers, + params=rendered_params, + timeout=self._timeout, + ) + text = req.text if self._is_on_template is not None: text = self._is_on_template.async_render_with_possible_json_value( diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 36514fc8f35..85cab6f1763 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[listen]==0.8.3"] + "requirements": ["ring-doorbell[listen]==0.8.5"] } diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 6a592c904f6..396fef5ac2e 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -419,7 +419,6 @@ class BlockSleepingClimate( class RpcClimate(ShellyRpcEntity, ClimateEntity): """Entity that controls a thermostat on RPC based Shelly devices.""" - _attr_hvac_modes = [HVACMode.OFF] _attr_icon = "mdi:thermostat" _attr_max_temp = RPC_THERMOSTAT_SETTINGS["max"] _attr_min_temp = RPC_THERMOSTAT_SETTINGS["min"] @@ -435,9 +434,9 @@ class RpcClimate(ShellyRpcEntity, ClimateEntity): "type", "heating" ) if self._thermostat_type == "cooling": - self._attr_hvac_modes.append(HVACMode.COOL) + self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL] else: - self._attr_hvac_modes.append(HVACMode.HEAT) + self._attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT] @property def target_temperature(self) -> float | None: diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 6b5c59f28db..d4164c1dd7d 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -367,7 +367,9 @@ def is_block_channel_type_light(settings: dict[str, Any], channel: int) -> bool: def is_rpc_channel_type_light(config: dict[str, Any], channel: int) -> bool: """Return true if rpc channel consumption type is set to light.""" con_types = config["sys"].get("ui_data", {}).get("consumption_types") - return con_types is not None and con_types[channel].lower().startswith("light") + if con_types is None or len(con_types) <= channel: + return False + return cast(str, con_types[channel]).lower().startswith("light") def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]: diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json index 89e018b6635..bcfd10d2f02 100644 --- a/homeassistant/components/surepetcare/manifest.json +++ b/homeassistant/components/surepetcare/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/surepetcare", "iot_class": "cloud_polling", "loggers": ["rich", "surepy"], - "requirements": ["surepy==0.8.0"] + "requirements": ["surepy==0.9.0"] } diff --git a/homeassistant/components/todo/intent.py b/homeassistant/components/todo/intent.py index ba3545d8dfd..4cf62c6391d 100644 --- a/homeassistant/components/todo/intent.py +++ b/homeassistant/components/todo/intent.py @@ -6,7 +6,7 @@ from homeassistant.helpers import intent import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent -from . import DOMAIN, TodoItem, TodoListEntity +from . import DOMAIN, TodoItem, TodoItemStatus, TodoListEntity INTENT_LIST_ADD_ITEM = "HassListAddItem" @@ -47,7 +47,9 @@ class ListAddItemIntent(intent.IntentHandler): assert target_list is not None # Add to list - await target_list.async_create_todo_item(TodoItem(item)) + await target_list.async_create_todo_item( + TodoItem(summary=item, status=TodoItemStatus.NEEDS_ACTION) + ) response = intent_obj.create_response() response.response_type = intent.IntentResponseType.ACTION_DONE diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 045538aa2d1..cd38f50bf6d 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -41,7 +41,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.22.0", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.22.3", "unifi-discovery==1.1.7"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/version/const.py b/homeassistant/components/version/const.py index 2dcb0028b27..0b39ecee604 100644 --- a/homeassistant/components/version/const.py +++ b/homeassistant/components/version/const.py @@ -66,6 +66,7 @@ BOARD_MAP: Final[dict[str, str]] = { "RaspberryPi 3 64bit": "rpi3-64", "RaspberryPi 4": "rpi4", "RaspberryPi 4 64bit": "rpi4-64", + "RaspberryPi 5": "rpi5-64", "ASUS Tinkerboard": "tinker", "ODROID C2": "odroid-c2", "ODROID C4": "odroid-c4", @@ -112,6 +113,7 @@ VALID_IMAGES: Final = [ "raspberrypi3", "raspberrypi4-64", "raspberrypi4", + "raspberrypi5-64", "tinker", ] diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index fe58ff044cd..a2965e782f4 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,15 +21,15 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.37.3", + "bellows==0.37.4", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.107", - "zigpy-deconz==0.22.2", - "zigpy==0.60.1", + "zha-quirks==0.0.108", + "zigpy-deconz==0.22.3", + "zigpy==0.60.2", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", - "zigpy-znp==0.12.0", + "zigpy-znp==0.12.1", "universal-silabs-flasher==0.0.15", "pyserial-asyncio-fast==0.11" ], diff --git a/homeassistant/const.py b/homeassistant/const.py index e4c6c2ed86a..b2538d0c87a 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 = 12 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __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/machine/raspberrypi5-64 b/machine/raspberrypi5-64 new file mode 100644 index 00000000000..2ed3b3c8e44 --- /dev/null +++ b/machine/raspberrypi5-64 @@ -0,0 +1,8 @@ +ARG \ + BUILD_FROM + +FROM $BUILD_FROM + +RUN apk --no-cache add \ + raspberrypi-userland \ + raspberrypi-userland-libs diff --git a/pyproject.toml b/pyproject.toml index c39c23819f4..a408e0de07e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.12.3" +version = "2023.12.4" 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 eb5034fdfd0..b65e6f60338 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,7 +191,7 @@ aioairq==0.3.1 aioairzone-cloud==0.3.6 # homeassistant.components.airzone -aioairzone==0.6.9 +aioairzone==0.7.2 # homeassistant.components.ambient_station aioambient==2023.04.0 @@ -318,7 +318,7 @@ aioopenexchangerates==0.4.0 aiopegelonline==0.0.6 # homeassistant.components.acmeda -aiopulse==0.4.3 +aiopulse==0.4.4 # homeassistant.components.purpleair aiopurpleair==2022.12.1 @@ -523,7 +523,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.37.3 +bellows==0.37.4 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 @@ -541,7 +541,7 @@ bleak==0.21.1 blebox-uniapi==2.2.0 # homeassistant.components.blink -blinkpy==0.22.3 +blinkpy==0.22.4 # homeassistant.components.bitcoin blockchain==1.4.4 @@ -681,7 +681,7 @@ demetriek==0.4.0 denonavr==0.11.4 # homeassistant.components.devialet -devialet==1.4.3 +devialet==1.4.5 # homeassistant.components.devolo_home_control devolo-home-control-api==0.18.2 @@ -1162,7 +1162,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==6.0.0 +life360==6.0.1 # homeassistant.components.osramlightify lightify==1.0.7.3 @@ -1261,7 +1261,7 @@ moehlenhoff-alpha2==1.3.0 mopeka-iot-ble==0.5.0 # homeassistant.components.motion_blinds -motionblinds==0.6.18 +motionblinds==0.6.19 # homeassistant.components.motioneye motioneye-client==0.3.14 @@ -1619,7 +1619,7 @@ pyasuswrt==0.1.20 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.6.0 +pyatmo==8.0.1 # homeassistant.components.apple_tv pyatv==0.14.3 @@ -1715,7 +1715,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.14.3 +pyenphase==1.15.2 # homeassistant.components.envisalink pyenvisalink==4.6 @@ -2245,7 +2245,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.22.0 +pyunifiprotect==4.22.3 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 @@ -2338,7 +2338,7 @@ renault-api==0.2.1 renson-endura-delta==1.6.0 # homeassistant.components.reolink -reolink-aio==0.8.2 +reolink-aio==0.8.4 # homeassistant.components.idteck_prox rfk101py==0.0.1 @@ -2347,7 +2347,7 @@ rfk101py==0.0.1 rflink==0.0.65 # homeassistant.components.ring -ring-doorbell[listen]==0.8.3 +ring-doorbell[listen]==0.8.5 # homeassistant.components.fleetgo ritassist==0.9.2 @@ -2537,7 +2537,7 @@ subarulink==0.7.9 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.8.0 +surepy==0.9.0 # homeassistant.components.swiss_hydrological_data swisshydrodata==0.1.0 @@ -2816,7 +2816,7 @@ zeroconf==0.128.5 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.107 +zha-quirks==0.0.108 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.9 @@ -2825,7 +2825,7 @@ zhong-hong-hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.22.2 +zigpy-deconz==0.22.3 # homeassistant.components.zha zigpy-xbee==0.20.1 @@ -2834,10 +2834,10 @@ zigpy-xbee==0.20.1 zigpy-zigate==0.12.0 # homeassistant.components.zha -zigpy-znp==0.12.0 +zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.60.1 +zigpy==0.60.2 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 593bea301cf..cbe772165d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairq==0.3.1 aioairzone-cloud==0.3.6 # homeassistant.components.airzone -aioairzone==0.6.9 +aioairzone==0.7.2 # homeassistant.components.ambient_station aioambient==2023.04.0 @@ -291,7 +291,7 @@ aioopenexchangerates==0.4.0 aiopegelonline==0.0.6 # homeassistant.components.acmeda -aiopulse==0.4.3 +aiopulse==0.4.4 # homeassistant.components.purpleair aiopurpleair==2022.12.1 @@ -445,7 +445,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.37.3 +bellows==0.37.4 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.14.6 @@ -460,7 +460,7 @@ bleak==0.21.1 blebox-uniapi==2.2.0 # homeassistant.components.blink -blinkpy==0.22.3 +blinkpy==0.22.4 # homeassistant.components.bluemaestro bluemaestro-ble==0.2.3 @@ -556,7 +556,7 @@ demetriek==0.4.0 denonavr==0.11.4 # homeassistant.components.devialet -devialet==1.4.3 +devialet==1.4.5 # homeassistant.components.devolo_home_control devolo-home-control-api==0.18.2 @@ -913,7 +913,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==6.0.0 +life360==6.0.1 # homeassistant.components.linear_garage_door linear-garage-door==0.2.7 @@ -985,7 +985,7 @@ moehlenhoff-alpha2==1.3.0 mopeka-iot-ble==0.5.0 # homeassistant.components.motion_blinds -motionblinds==0.6.18 +motionblinds==0.6.19 # homeassistant.components.motioneye motioneye-client==0.3.14 @@ -1235,7 +1235,7 @@ pyasuswrt==0.1.20 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.6.0 +pyatmo==8.0.1 # homeassistant.components.apple_tv pyatv==0.14.3 @@ -1295,7 +1295,7 @@ pyeconet==0.1.22 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.14.3 +pyenphase==1.15.2 # homeassistant.components.everlights pyeverlights==0.1.0 @@ -1681,7 +1681,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.22.0 +pyunifiprotect==4.22.3 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 @@ -1750,13 +1750,13 @@ renault-api==0.2.1 renson-endura-delta==1.6.0 # homeassistant.components.reolink -reolink-aio==0.8.2 +reolink-aio==0.8.4 # homeassistant.components.rflink rflink==0.0.65 # homeassistant.components.ring -ring-doorbell[listen]==0.8.3 +ring-doorbell[listen]==0.8.5 # homeassistant.components.roku rokuecp==0.18.1 @@ -1898,7 +1898,7 @@ subarulink==0.7.9 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.8.0 +surepy==0.9.0 # homeassistant.components.switchbot_cloud switchbot-api==1.2.1 @@ -2111,10 +2111,10 @@ zeroconf==0.128.5 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.107 +zha-quirks==0.0.108 # homeassistant.components.zha -zigpy-deconz==0.22.2 +zigpy-deconz==0.22.3 # homeassistant.components.zha zigpy-xbee==0.20.1 @@ -2123,10 +2123,10 @@ zigpy-xbee==0.20.1 zigpy-zigate==0.12.0 # homeassistant.components.zha -zigpy-znp==0.12.0 +zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.60.1 +zigpy==0.60.2 # homeassistant.components.zwave_js zwave-js-server-python==0.54.0 diff --git a/tests/components/airzone/snapshots/test_diagnostics.ambr b/tests/components/airzone/snapshots/test_diagnostics.ambr index 9cb6e550711..0eab9ffe81b 100644 --- a/tests/components/airzone/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone/snapshots/test_diagnostics.ambr @@ -188,7 +188,7 @@ 'coldStages': 0, 'coolmaxtemp': 90, 'coolmintemp': 64, - 'coolsetpoint': 73, + 'coolsetpoint': 77, 'errors': list([ ]), 'floor_demand': 0, @@ -196,7 +196,7 @@ 'heatStages': 0, 'heatmaxtemp': 86, 'heatmintemp': 50, - 'heatsetpoint': 77, + 'heatsetpoint': 73, 'humidity': 0, 'maxTemp': 90, 'minTemp': 64, @@ -601,7 +601,7 @@ 1, ]), 'demand': False, - 'double-set-point': True, + 'double-set-point': False, 'full-name': 'Airzone [2:1] Airzone 2:1', 'heat-stage': 1, 'heat-stages': list([ @@ -644,7 +644,7 @@ 'cold-stage': 0, 'cool-temp-max': 90.0, 'cool-temp-min': 64.0, - 'cool-temp-set': 73.0, + 'cool-temp-set': 77.0, 'demand': True, 'double-set-point': True, 'floor-demand': False, @@ -652,7 +652,7 @@ 'heat-stage': 0, 'heat-temp-max': 86.0, 'heat-temp-min': 50.0, - 'heat-temp-set': 77.0, + 'heat-temp-set': 73.0, 'id': 1, 'master': True, 'mode': 7, diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py index 34844e34370..f33d1a8b28a 100644 --- a/tests/components/airzone/test_climate.py +++ b/tests/components/airzone/test_climate.py @@ -221,7 +221,8 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_MAX_TEMP) == 32.2 assert state.attributes.get(ATTR_MIN_TEMP) == 17.8 assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP - assert state.attributes.get(ATTR_TEMPERATURE) == 22.8 + assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25.0 + assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 22.8 HVAC_MOCK_CHANGED = copy.deepcopy(HVAC_MOCK) HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MAX_TEMP] = 25 @@ -594,8 +595,8 @@ async def test_airzone_climate_set_temp_range(hass: HomeAssistant) -> None: { API_SYSTEM_ID: 3, API_ZONE_ID: 1, - API_COOL_SET_POINT: 68.0, - API_HEAT_SET_POINT: 77.0, + API_COOL_SET_POINT: 77.0, + API_HEAT_SET_POINT: 68.0, } ] } @@ -618,5 +619,5 @@ async def test_airzone_climate_set_temp_range(hass: HomeAssistant) -> None: ) state = hass.states.get("climate.dkn_plus") - assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 20.0 - assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 25.0 + assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25.0 + assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 20.0 diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index a3454549e05..f83eceaae9c 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -245,10 +245,10 @@ HVAC_MOCK = { API_ZONE_ID: 1, API_NAME: "DKN Plus", API_ON: 1, - API_COOL_SET_POINT: 73, + API_COOL_SET_POINT: 77, API_COOL_MAX_TEMP: 90, API_COOL_MIN_TEMP: 64, - API_HEAT_SET_POINT: 77, + API_HEAT_SET_POINT: 73, API_HEAT_MAX_TEMP: 86, API_HEAT_MIN_TEMP: 50, API_MAX_TEMP: 90, diff --git a/tests/components/blink/conftest.py b/tests/components/blink/conftest.py index 946840c23b9..d7deaf39bd9 100644 --- a/tests/components/blink/conftest.py +++ b/tests/components/blink/conftest.py @@ -87,6 +87,7 @@ def mock_config_fixture(): "device_id": "Home Assistant", "uid": "BlinkCamera_e1233333e2-0909-09cd-777a-123456789012", "token": "A_token", + "unique_id": "an_email@email.com", "host": "u034.immedia-semi.com", "region_id": "u034", "client_id": 123456, diff --git a/tests/components/blink/snapshots/test_diagnostics.ambr b/tests/components/blink/snapshots/test_diagnostics.ambr index 7fb13c97548..b572aae0a00 100644 --- a/tests/components/blink/snapshots/test_diagnostics.ambr +++ b/tests/components/blink/snapshots/test_diagnostics.ambr @@ -34,6 +34,7 @@ 'region_id': 'u034', 'token': '**REDACTED**', 'uid': 'BlinkCamera_e1233333e2-0909-09cd-777a-123456789012', + 'unique_id': '**REDACTED**', 'username': '**REDACTED**', }), 'disabled_by': None, diff --git a/tests/components/fritz/test_image.py b/tests/components/fritz/test_image.py index cbcbded5692..da5b8a76d27 100644 --- a/tests/components/fritz/test_image.py +++ b/tests/components/fritz/test_image.py @@ -4,12 +4,13 @@ from http import HTTPStatus from unittest.mock import patch import pytest +from requests.exceptions import ReadTimeout from syrupy.assertion import SnapshotAssertion from homeassistant.components.fritz.const import DOMAIN from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import Platform +from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry from homeassistant.setup import async_setup_component @@ -170,3 +171,43 @@ async def test_image_update( assert resp_body != resp_body_new assert resp_body_new == snapshot + + +@pytest.mark.parametrize(("fc_data"), [({**MOCK_FB_SERVICES, **GUEST_WIFI_ENABLED})]) +async def test_image_update_unavailable( + hass: HomeAssistant, + fc_class_mock, + fh_class_mock, +) -> None: + """Test image update when fritzbox is unavailable.""" + + # setup component with image platform only + with patch( + "homeassistant.components.fritz.PLATFORMS", + [Platform.IMAGE], + ): + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + state = hass.states.get("image.mock_title_guestwifi") + assert state + + # fritzbox becomes unavailable + fc_class_mock().call_action_side_effect(ReadTimeout) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) + await hass.async_block_till_done() + + state = hass.states.get("image.mock_title_guestwifi") + assert state.state == STATE_UNKNOWN + + # fritzbox is available again + fc_class_mock().call_action_side_effect(None) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) + await hass.async_block_till_done() + + state = hass.states.get("image.mock_title_guestwifi") + assert state.state != STATE_UNKNOWN diff --git a/tests/components/netatmo/fixtures/getstationsdata.json b/tests/components/netatmo/fixtures/getstationsdata.json index 10c3ca85e06..b0da0820699 100644 --- a/tests/components/netatmo/fixtures/getstationsdata.json +++ b/tests/components/netatmo/fixtures/getstationsdata.json @@ -475,22 +475,12 @@ "last_setup": 1558709954, "data_type": ["Temperature", "Humidity"], "battery_percent": 27, - "reachable": true, + "reachable": false, "firmware": 50, "last_message": 1644582699, "last_seen": 1644582699, "rf_status": 68, - "battery_vp": 4678, - "dashboard_data": { - "time_utc": 1644582648, - "Temperature": 9.4, - "Humidity": 57, - "min_temp": 6.7, - "max_temp": 9.8, - "date_max_temp": 1644534223, - "date_min_temp": 1644569369, - "temp_trend": "up" - } + "battery_vp": 4678 }, { "_id": "12:34:56:80:c1:ea", diff --git a/tests/components/netatmo/snapshots/test_diagnostics.ambr b/tests/components/netatmo/snapshots/test_diagnostics.ambr index bd9005bd389..0dd424ec7d8 100644 --- a/tests/components/netatmo/snapshots/test_diagnostics.ambr +++ b/tests/components/netatmo/snapshots/test_diagnostics.ambr @@ -561,26 +561,28 @@ 'access_doorbell', 'access_presence', 'read_bubendorff', + 'read_bfi', 'read_camera', 'read_carbonmonoxidedetector', 'read_doorbell', 'read_homecoach', 'read_magellan', + 'read_mhs1', 'read_mx', 'read_presence', 'read_smarther', 'read_smokedetector', 'read_station', 'read_thermostat', - 'read_mhs1', 'write_bubendorff', + 'write_bfi', 'write_camera', 'write_magellan', + 'write_mhs1', 'write_mx', 'write_presence', 'write_smarther', 'write_thermostat', - 'write_mhs1', ]), 'type': 'Bearer', }), diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index 00cec6f8aa0..ce35873c3e5 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -10,8 +10,8 @@ from homeassistant.helpers import entity_registry as er from .common import TEST_TIME, selected_platforms -async def test_weather_sensor(hass: HomeAssistant, config_entry, netatmo_auth) -> None: - """Test weather sensor setup.""" +async def test_indoor_sensor(hass: HomeAssistant, config_entry, netatmo_auth) -> None: + """Test indoor sensor setup.""" with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -25,6 +25,18 @@ async def test_weather_sensor(hass: HomeAssistant, config_entry, netatmo_auth) - assert hass.states.get(f"{prefix}pressure").state == "1014.5" +async def test_weather_sensor(hass: HomeAssistant, config_entry, netatmo_auth) -> None: + """Test weather sensor unreachable.""" + with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + prefix = "sensor.villa_outdoor_" + + assert hass.states.get(f"{prefix}temperature").state == "unavailable" + + async def test_public_weather_sensor( hass: HomeAssistant, config_entry, netatmo_auth ) -> None: diff --git a/tests/components/ourgroceries/__init__.py b/tests/components/ourgroceries/__init__.py index 67fcb439908..6f90cb7ea1b 100644 --- a/tests/components/ourgroceries/__init__.py +++ b/tests/components/ourgroceries/__init__.py @@ -1,6 +1,6 @@ """Tests for the OurGroceries integration.""" -def items_to_shopping_list(items: list) -> dict[dict[list]]: +def items_to_shopping_list(items: list, version_id: str = "1") -> dict[dict[list]]: """Convert a list of items into a shopping list.""" - return {"list": {"items": items}} + return {"list": {"versionId": version_id, "items": items}} diff --git a/tests/components/ourgroceries/conftest.py b/tests/components/ourgroceries/conftest.py index 7f113da2633..c5fdec3ecb7 100644 --- a/tests/components/ourgroceries/conftest.py +++ b/tests/components/ourgroceries/conftest.py @@ -46,7 +46,7 @@ def mock_ourgroceries(items: list[dict]) -> AsyncMock: og = AsyncMock() og.login.return_value = True og.get_my_lists.return_value = { - "shoppingLists": [{"id": "test_list", "name": "Test List"}] + "shoppingLists": [{"id": "test_list", "name": "Test List", "versionId": "1"}] } og.get_list_items.return_value = items_to_shopping_list(items) return og diff --git a/tests/components/ourgroceries/test_todo.py b/tests/components/ourgroceries/test_todo.py index 8686c52d79b..649e86f2b05 100644 --- a/tests/components/ourgroceries/test_todo.py +++ b/tests/components/ourgroceries/test_todo.py @@ -17,6 +17,10 @@ from . import items_to_shopping_list from tests.common import async_fire_time_changed +def _mock_version_id(og: AsyncMock, version: int) -> None: + og.get_my_lists.return_value["shoppingLists"][0]["versionId"] = str(version) + + @pytest.mark.parametrize( ("items", "expected_state"), [ @@ -57,8 +61,10 @@ async def test_add_todo_list_item( ourgroceries.add_item_to_list = AsyncMock() # Fake API response when state is refreshed after create + _mock_version_id(ourgroceries, 2) ourgroceries.get_list_items.return_value = items_to_shopping_list( - [{"id": "12345", "name": "Soda"}] + [{"id": "12345", "name": "Soda"}], + version_id="2", ) await hass.services.async_call( @@ -95,6 +101,7 @@ async def test_update_todo_item_status( ourgroceries.toggle_item_crossed_off = AsyncMock() # Fake API response when state is refreshed after crossing off + _mock_version_id(ourgroceries, 2) ourgroceries.get_list_items.return_value = items_to_shopping_list( [{"id": "12345", "name": "Soda", "crossedOffAt": 1699107501}] ) @@ -118,6 +125,7 @@ async def test_update_todo_item_status( assert state.state == "0" # Fake API response when state is refreshed after reopen + _mock_version_id(ourgroceries, 2) ourgroceries.get_list_items.return_value = items_to_shopping_list( [{"id": "12345", "name": "Soda"}] ) @@ -166,6 +174,7 @@ async def test_update_todo_item_summary( ourgroceries.change_item_on_list = AsyncMock() # Fake API response when state is refreshed update + _mock_version_id(ourgroceries, 2) ourgroceries.get_list_items.return_value = items_to_shopping_list( [{"id": "12345", "name": "Milk"}] ) @@ -204,6 +213,7 @@ async def test_remove_todo_item( ourgroceries.remove_item_from_list = AsyncMock() # Fake API response when state is refreshed after remove + _mock_version_id(ourgroceries, 2) ourgroceries.get_list_items.return_value = items_to_shopping_list([]) await hass.services.async_call( @@ -224,6 +234,25 @@ async def test_remove_todo_item( assert state.state == "0" +async def test_version_id_optimization( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + setup_integration: None, + ourgroceries: AsyncMock, +) -> None: + """Test that list items aren't being retrieved if version id stays the same.""" + state = hass.states.get("todo.test_list") + assert state.state == "0" + assert ourgroceries.get_list_items.call_count == 1 + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("todo.test_list") + assert state.state == "0" + assert ourgroceries.get_list_items.call_count == 1 + + @pytest.mark.parametrize( ("exception"), [ @@ -242,6 +271,7 @@ async def test_coordinator_error( state = hass.states.get("todo.test_list") assert state.state == "0" + _mock_version_id(ourgroceries, 2) ourgroceries.get_list_items.side_effect = exception freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index df90af44e73..cc591573bd6 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -1,5 +1,4 @@ """The tests for the REST switch platform.""" -import asyncio from http import HTTPStatus import httpx @@ -84,7 +83,7 @@ async def test_setup_failed_connect( caplog: pytest.LogCaptureFixture, ) -> None: """Test setup when connection error occurs.""" - respx.get(RESOURCE).mock(side_effect=asyncio.TimeoutError()) + respx.get(RESOURCE).mock(side_effect=httpx.ConnectError("")) config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}} assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() @@ -98,7 +97,7 @@ async def test_setup_timeout( caplog: pytest.LogCaptureFixture, ) -> None: """Test setup when connection timeout occurs.""" - respx.get(RESOURCE).mock(side_effect=asyncio.TimeoutError()) + respx.get(RESOURCE).mock(side_effect=httpx.TimeoutException("")) config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}} assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() @@ -304,7 +303,7 @@ async def test_turn_on_timeout(hass: HomeAssistant) -> None: """Test turn_on when timeout occurs.""" await _async_setup_test_switch(hass) - respx.post(RESOURCE) % HTTPStatus.INTERNAL_SERVER_ERROR + respx.post(RESOURCE).mock(side_effect=httpx.TimeoutException("")) await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -364,7 +363,7 @@ async def test_turn_off_timeout(hass: HomeAssistant) -> None: """Test turn_off when timeout occurs.""" await _async_setup_test_switch(hass) - respx.post(RESOURCE).mock(side_effect=asyncio.TimeoutError()) + respx.post(RESOURCE).mock(side_effect=httpx.TimeoutException("")) await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -417,7 +416,7 @@ async def test_update_timeout(hass: HomeAssistant) -> None: """Test update when timeout occurs.""" await _async_setup_test_switch(hass) - respx.get(RESOURCE).mock(side_effect=asyncio.TimeoutError()) + respx.get(RESOURCE).mock(side_effect=httpx.TimeoutException("")) async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 90b06858e00..0edca7a7ef6 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -1038,6 +1038,7 @@ async def test_add_item_intent( assert len(entity1.items) == 1 assert len(entity2.items) == 0 assert entity1.items[0].summary == "beer" + assert entity1.items[0].status == TodoItemStatus.NEEDS_ACTION entity1.items.clear() # Add to second list @@ -1052,6 +1053,7 @@ async def test_add_item_intent( assert len(entity1.items) == 0 assert len(entity2.items) == 1 assert entity2.items[0].summary == "cheese" + assert entity2.items[0].status == TodoItemStatus.NEEDS_ACTION # List name is case insensitive response = await intent.async_handle( @@ -1065,6 +1067,7 @@ async def test_add_item_intent( assert len(entity1.items) == 0 assert len(entity2.items) == 2 assert entity2.items[1].summary == "wine" + assert entity2.items[1].status == TodoItemStatus.NEEDS_ACTION # Missing list with pytest.raises(intent.IntentHandleError):