This commit is contained in:
Franck Nijhof 2023-12-27 11:06:09 +01:00 committed by GitHub
commit ae80d576bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 282 additions and 166 deletions

View File

@ -197,7 +197,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2023.09.0 uses: home-assistant/builder@2023.12.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@ -247,6 +247,7 @@ jobs:
- raspberrypi3-64 - raspberrypi3-64
- raspberrypi4 - raspberrypi4
- raspberrypi4-64 - raspberrypi4-64
- raspberrypi5-64
- tinker - tinker
- yellow - yellow
- green - green
@ -273,7 +274,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2023.09.0 uses: home-assistant/builder@2023.12.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/acmeda", "documentation": "https://www.home-assistant.io/integrations/acmeda",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiopulse"], "loggers": ["aiopulse"],
"requirements": ["aiopulse==0.4.3"] "requirements": ["aiopulse==0.4.4"]
} }

View File

@ -248,7 +248,6 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
self._attr_hvac_mode = HVACMode.OFF self._attr_hvac_mode = HVACMode.OFF
self._attr_max_temp = self.get_airzone_value(AZD_TEMP_MAX) self._attr_max_temp = self.get_airzone_value(AZD_TEMP_MAX)
self._attr_min_temp = self.get_airzone_value(AZD_TEMP_MIN) 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: if self.supported_features & ClimateEntityFeature.FAN_MODE:
self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED)) self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED))
if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE: 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( self._attr_target_temperature_low = self.get_airzone_value(
AZD_HEAT_TEMP_SET AZD_HEAT_TEMP_SET
) )
else:
self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET)

View File

@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone", "documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairzone"], "loggers": ["aioairzone"],
"requirements": ["aioairzone==0.6.9"] "requirements": ["aioairzone==0.7.2"]
} }

View File

@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant
from .const import DOMAIN 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( async def async_get_config_entry_diagnostics(

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/blink", "documentation": "https://www.home-assistant.io/integrations/blink",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["blinkpy"], "loggers": ["blinkpy"],
"requirements": ["blinkpy==0.22.3"] "requirements": ["blinkpy==0.22.4"]
} }

View File

@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/devialet", "documentation": "https://www.home-assistant.io/integrations/devialet",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["devialet==1.4.3"], "requirements": ["devialet==1.4.5"],
"zeroconf": ["_devialet-http._tcp.local."] "zeroconf": ["_devialet-http._tcp.local."]
} }

View File

@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pyenphase"], "loggers": ["pyenphase"],
"requirements": ["pyenphase==1.14.3"], "requirements": ["pyenphase==1.15.2"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_enphase-envoy._tcp.local." "type": "_enphase-envoy._tcp.local."

View File

@ -5,6 +5,8 @@ from __future__ import annotations
from io import BytesIO from io import BytesIO
import logging import logging
from requests.exceptions import RequestException
from homeassistant.components.image import ImageEntity from homeassistant.components.image import ImageEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
@ -78,7 +80,13 @@ class FritzGuestWifiQRImage(FritzBoxBaseEntity, ImageEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update the image entity data.""" """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: if self._current_qr_bytes != qr_bytes:
dt_now = dt_util.utcnow() dt_now = dt_util.utcnow()

View File

@ -85,7 +85,7 @@ async def async_setup_entry(
entry.async_on_unload(coordinator.async_add_listener(_add_entities)) 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): class FritzboxBinarySensor(FritzBoxDeviceEntity, BinarySensorEntity):

View File

@ -29,7 +29,7 @@ async def async_setup_entry(
entry.async_on_unload(coordinator.async_add_listener(_add_entities)) 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): class FritzBoxTemplate(FritzBoxEntity, ButtonEntity):

View File

@ -66,7 +66,7 @@ async def async_setup_entry(
entry.async_on_unload(coordinator.async_add_listener(_add_entities)) 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): class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):

View File

@ -38,7 +38,7 @@ async def async_setup_entry(
entry.async_on_unload(coordinator.async_add_listener(_add_entities)) 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): class FritzboxCover(FritzBoxDeviceEntity, CoverEntity):

View File

@ -44,7 +44,7 @@ async def async_setup_entry(
entry.async_on_unload(coordinator.async_add_listener(_add_entities)) 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): class FritzboxLight(FritzBoxDeviceEntity, LightEntity):

View File

@ -230,7 +230,7 @@ async def async_setup_entry(
entry.async_on_unload(coordinator.async_add_listener(_add_entities)) 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): class FritzBoxSensor(FritzBoxDeviceEntity, SensorEntity):

View File

@ -33,7 +33,7 @@ async def async_setup_entry(
entry.async_on_unload(coordinator.async_add_listener(_add_entities)) 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): class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity):

View File

@ -270,6 +270,7 @@ HARDWARE_INTEGRATIONS = {
"rpi3-64": "raspberry_pi", "rpi3-64": "raspberry_pi",
"rpi4": "raspberry_pi", "rpi4": "raspberry_pi",
"rpi4-64": "raspberry_pi", "rpi4-64": "raspberry_pi",
"rpi5-64": "raspberry_pi",
"yellow": "homeassistant_yellow", "yellow": "homeassistant_yellow",
} }

View File

@ -507,6 +507,7 @@ class HoneywellUSThermostat(ClimateEntity):
except ( except (
AuthError, AuthError,
ClientConnectionError, ClientConnectionError,
AscConnectionError,
asyncio.TimeoutError, asyncio.TimeoutError,
): ):
self._retry += 1 self._retry += 1

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/life360", "documentation": "https://www.home-assistant.io/integrations/life360",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["life360"], "loggers": ["life360"],
"requirements": ["life360==6.0.0"] "requirements": ["life360==6.0.1"]
} }

View File

@ -21,5 +21,5 @@
"documentation": "https://www.home-assistant.io/integrations/motion_blinds", "documentation": "https://www.home-assistant.io/integrations/motion_blinds",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["motionblinds"], "loggers": ["motionblinds"],
"requirements": ["motionblinds==0.6.18"] "requirements": ["motionblinds==0.6.19"]
} }

View File

@ -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: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn light on.""" """Turn light on."""
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
@ -211,6 +206,8 @@ class NetatmoLight(NetatmoBase, LightEntity):
@callback @callback
def async_update_callback(self) -> None: def async_update_callback(self) -> None:
"""Update the entity's state.""" """Update the entity's state."""
self._attr_is_on = self._dimmer.on is True
if self._dimmer.brightness is not None: if self._dimmer.brightness is not None:
# Netatmo uses a range of [0, 100] to control brightness # Netatmo uses a range of [0, 100] to control brightness
self._attr_brightness = round((self._dimmer.brightness / 100) * 255) self._attr_brightness = round((self._dimmer.brightness / 100) * 255)

View File

@ -12,5 +12,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyatmo"], "loggers": ["pyatmo"],
"requirements": ["pyatmo==7.6.0"] "requirements": ["pyatmo==8.0.1"]
} }

View File

@ -447,17 +447,16 @@ class NetatmoWeatherSensor(NetatmoBase, SensorEntity):
} }
) )
@property
def available(self) -> bool:
"""Return entity availability."""
return self.state is not None
@callback @callback
def async_update_callback(self) -> None: def async_update_callback(self) -> None:
"""Update the entity's state.""" """Update the entity's state."""
if ( if (
state := getattr(self._module, self.entity_description.netatmo_name) not self._module.reachable
) is None: or (state := getattr(self._module, self.entity_description.netatmo_name))
is None
):
if self.available:
self._attr_available = False
return return
if self.entity_description.netatmo_name in { if self.entity_description.netatmo_name in {
@ -475,6 +474,7 @@ class NetatmoWeatherSensor(NetatmoBase, SensorEntity):
else: else:
self._attr_native_value = state self._attr_native_value = state
self._attr_available = True
self.async_write_ha_state() self.async_write_ha_state()
@ -519,7 +519,6 @@ class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity):
if not self._module.reachable: if not self._module.reachable:
if self.available: if self.available:
self._attr_available = False self._attr_available = False
self._attr_native_value = None
return return
self._attr_available = True self._attr_available = True
@ -565,9 +564,15 @@ class NetatmoSensor(NetatmoBase, SensorEntity):
@callback @callback
def async_update_callback(self) -> None: def async_update_callback(self) -> None:
"""Update the entity's state.""" """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: if (state := getattr(self._module, self.entity_description.key)) is None:
return return
self._attr_available = True
self._attr_native_value = state self._attr_native_value = state
self.async_write_ha_state() self.async_write_ha_state()
@ -777,7 +782,6 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
self.entity_description.key, self.entity_description.key,
self._area_name, self._area_name,
) )
self._attr_native_value = None
self._attr_available = False self._attr_available = False
return return

View File

@ -290,17 +290,19 @@ class NotionEntity(CoordinatorEntity[DataUpdateCoordinator[NotionData]]):
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(coordinator) super().__init__(coordinator)
bridge = self.coordinator.data.bridges[bridge_id]
sensor = self.coordinator.data.sensors[sensor_id] sensor = self.coordinator.data.sensors[sensor_id]
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, sensor.hardware_id)}, identifiers={(DOMAIN, sensor.hardware_id)},
manufacturer="Silicon Labs", manufacturer="Silicon Labs",
model=str(sensor.hardware_revision), model=str(sensor.hardware_revision),
name=str(sensor.name).capitalize(), name=str(sensor.name).capitalize(),
sw_version=sensor.firmware_version, 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_extra_state_attributes = {}
self._attr_unique_id = listener_id self._attr_unique_id = listener_id
self._bridge_id = bridge_id self._bridge_id = bridge_id
@ -322,6 +324,14 @@ class NotionEntity(CoordinatorEntity[DataUpdateCoordinator[NotionData]]):
"""Return the listener related to this entity.""" """Return the listener related to this entity."""
return self.coordinator.data.listeners[self._listener_id] 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 @callback
def _async_update_bridge_id(self) -> None: def _async_update_bridge_id(self) -> None:
"""Update the entity's bridge ID if it has changed. """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] 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 # If the bridge ID hasn't changed, return:
# to a bridge that doesn't exist (which can happen due to a Notion API bug), if self._bridge_id == sensor.bridge.id:
# return immediately: return
if (
self._bridge_id == sensor.bridge.id # If the bridge doesn't exist, return:
or sensor.bridge.id not in self.coordinator.data.bridges if (bridge := self._async_get_bridge(sensor.bridge.id)) is None:
):
return return
self._bridge_id = sensor.bridge.id self._bridge_id = sensor.bridge.id

View File

@ -24,16 +24,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
data = entry.data data = entry.data
og = OurGroceries(data[CONF_USERNAME], data[CONF_PASSWORD]) og = OurGroceries(data[CONF_USERNAME], data[CONF_PASSWORD])
lists = []
try: try:
await og.login() await og.login()
lists = (await og.get_my_lists())["shoppingLists"]
except (AsyncIOTimeoutError, ClientError) as error: except (AsyncIOTimeoutError, ClientError) as error:
raise ConfigEntryNotReady from error raise ConfigEntryNotReady from error
except InvalidLoginException: except InvalidLoginException:
return False return False
coordinator = OurGroceriesDataUpdateCoordinator(hass, og, lists) coordinator = OurGroceriesDataUpdateCoordinator(hass, og)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = coordinator hass.data[DOMAIN][entry.entry_id] = coordinator

View File

@ -20,13 +20,11 @@ _LOGGER = logging.getLogger(__name__)
class OurGroceriesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): class OurGroceriesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
"""Class to manage fetching OurGroceries data.""" """Class to manage fetching OurGroceries data."""
def __init__( def __init__(self, hass: HomeAssistant, og: OurGroceries) -> None:
self, hass: HomeAssistant, og: OurGroceries, lists: list[dict]
) -> None:
"""Initialize global OurGroceries data updater.""" """Initialize global OurGroceries data updater."""
self.og = og self.og = og
self.lists = lists self.lists: list[dict] = []
self._ids = [sl["id"] for sl in lists] self._cache: dict[str, dict] = {}
interval = timedelta(seconds=SCAN_INTERVAL) interval = timedelta(seconds=SCAN_INTERVAL)
super().__init__( super().__init__(
hass, hass,
@ -35,13 +33,16 @@ class OurGroceriesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
update_interval=interval, 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]: async def _async_update_data(self) -> dict[str, dict]:
"""Fetch data from OurGroceries.""" """Fetch data from OurGroceries."""
return dict( self.lists = (await self.og.get_my_lists())["shoppingLists"]
zip( await asyncio.gather(
self._ids, *[self._update_list(sl["id"], sl["versionId"]) for sl in self.lists]
await asyncio.gather(
*[self.og.get_list_items(list_id=id) for id in self._ids]
),
)
) )
return self._cache

View File

@ -17,6 +17,7 @@ BOARD_NAMES = {
"rpi3-64": "Raspberry Pi 3", "rpi3-64": "Raspberry Pi 3",
"rpi4": "Raspberry Pi 4 (32-bit)", "rpi4": "Raspberry Pi 4 (32-bit)",
"rpi4-64": "Raspberry Pi 4", "rpi4-64": "Raspberry Pi 4",
"rpi5-64": "Raspberry Pi 5",
} }
MODELS = { MODELS = {
@ -28,6 +29,7 @@ MODELS = {
"rpi3-64": "3", "rpi3-64": "3",
"rpi4": "4", "rpi4": "4",
"rpi4-64": "4", "rpi4-64": "4",
"rpi5-64": "5",
} }

View File

@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink", "documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["reolink_aio"], "loggers": ["reolink_aio"],
"requirements": ["reolink-aio==0.8.2"] "requirements": ["reolink-aio==0.8.4"]
} }

View File

@ -202,22 +202,22 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity):
rendered_headers = template.render_complex(self._headers, parse_result=False) rendered_headers = template.render_complex(self._headers, parse_result=False)
rendered_params = template.render_complex(self._params) rendered_params = template.render_complex(self._params)
async with asyncio.timeout(self._timeout): req: httpx.Response = await getattr(websession, self._method)(
req: httpx.Response = await getattr(websession, self._method)( self._resource,
self._resource, auth=self._auth,
auth=self._auth, content=bytes(body, "utf-8"),
content=bytes(body, "utf-8"), headers=rendered_headers,
headers=rendered_headers, params=rendered_params,
params=rendered_params, timeout=self._timeout,
) )
return req return req
async def async_update(self) -> None: async def async_update(self) -> None:
"""Get the current state, catching errors.""" """Get the current state, catching errors."""
req = None req = None
try: try:
req = await self.get_device_state(self.hass) req = await self.get_device_state(self.hass)
except asyncio.TimeoutError: except (asyncio.TimeoutError, httpx.TimeoutException):
_LOGGER.exception("Timed out while fetching data") _LOGGER.exception("Timed out while fetching data")
except httpx.RequestError as err: except httpx.RequestError as err:
_LOGGER.exception("Error while fetching data: %s", 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_headers = template.render_complex(self._headers, parse_result=False)
rendered_params = template.render_complex(self._params) rendered_params = template.render_complex(self._params)
async with asyncio.timeout(self._timeout): req = await websession.get(
req = await websession.get( self._state_resource,
self._state_resource, auth=self._auth,
auth=self._auth, headers=rendered_headers,
headers=rendered_headers, params=rendered_params,
params=rendered_params, timeout=self._timeout,
) )
text = req.text text = req.text
if self._is_on_template is not None: if self._is_on_template is not None:
text = self._is_on_template.async_render_with_possible_json_value( text = self._is_on_template.async_render_with_possible_json_value(

View File

@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/ring", "documentation": "https://www.home-assistant.io/integrations/ring",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["ring_doorbell"], "loggers": ["ring_doorbell"],
"requirements": ["ring-doorbell[listen]==0.8.3"] "requirements": ["ring-doorbell[listen]==0.8.5"]
} }

View File

@ -419,7 +419,6 @@ class BlockSleepingClimate(
class RpcClimate(ShellyRpcEntity, ClimateEntity): class RpcClimate(ShellyRpcEntity, ClimateEntity):
"""Entity that controls a thermostat on RPC based Shelly devices.""" """Entity that controls a thermostat on RPC based Shelly devices."""
_attr_hvac_modes = [HVACMode.OFF]
_attr_icon = "mdi:thermostat" _attr_icon = "mdi:thermostat"
_attr_max_temp = RPC_THERMOSTAT_SETTINGS["max"] _attr_max_temp = RPC_THERMOSTAT_SETTINGS["max"]
_attr_min_temp = RPC_THERMOSTAT_SETTINGS["min"] _attr_min_temp = RPC_THERMOSTAT_SETTINGS["min"]
@ -435,9 +434,9 @@ class RpcClimate(ShellyRpcEntity, ClimateEntity):
"type", "heating" "type", "heating"
) )
if self._thermostat_type == "cooling": if self._thermostat_type == "cooling":
self._attr_hvac_modes.append(HVACMode.COOL) self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL]
else: else:
self._attr_hvac_modes.append(HVACMode.HEAT) self._attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT]
@property @property
def target_temperature(self) -> float | None: def target_temperature(self) -> float | None:

View File

@ -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: def is_rpc_channel_type_light(config: dict[str, Any], channel: int) -> bool:
"""Return true if rpc channel consumption type is set to light.""" """Return true if rpc channel consumption type is set to light."""
con_types = config["sys"].get("ui_data", {}).get("consumption_types") 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]]: def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]:

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/surepetcare", "documentation": "https://www.home-assistant.io/integrations/surepetcare",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["rich", "surepy"], "loggers": ["rich", "surepy"],
"requirements": ["surepy==0.8.0"] "requirements": ["surepy==0.9.0"]
} }

View File

@ -6,7 +6,7 @@ from homeassistant.helpers import intent
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from . import DOMAIN, TodoItem, TodoListEntity from . import DOMAIN, TodoItem, TodoItemStatus, TodoListEntity
INTENT_LIST_ADD_ITEM = "HassListAddItem" INTENT_LIST_ADD_ITEM = "HassListAddItem"
@ -47,7 +47,9 @@ class ListAddItemIntent(intent.IntentHandler):
assert target_list is not None assert target_list is not None
# Add to list # 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 = intent_obj.create_response()
response.response_type = intent.IntentResponseType.ACTION_DONE response.response_type = intent.IntentResponseType.ACTION_DONE

View File

@ -41,7 +41,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyunifiprotect", "unifi_discovery"], "loggers": ["pyunifiprotect", "unifi_discovery"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["pyunifiprotect==4.22.0", "unifi-discovery==1.1.7"], "requirements": ["pyunifiprotect==4.22.3", "unifi-discovery==1.1.7"],
"ssdp": [ "ssdp": [
{ {
"manufacturer": "Ubiquiti Networks", "manufacturer": "Ubiquiti Networks",

View File

@ -66,6 +66,7 @@ BOARD_MAP: Final[dict[str, str]] = {
"RaspberryPi 3 64bit": "rpi3-64", "RaspberryPi 3 64bit": "rpi3-64",
"RaspberryPi 4": "rpi4", "RaspberryPi 4": "rpi4",
"RaspberryPi 4 64bit": "rpi4-64", "RaspberryPi 4 64bit": "rpi4-64",
"RaspberryPi 5": "rpi5-64",
"ASUS Tinkerboard": "tinker", "ASUS Tinkerboard": "tinker",
"ODROID C2": "odroid-c2", "ODROID C2": "odroid-c2",
"ODROID C4": "odroid-c4", "ODROID C4": "odroid-c4",
@ -112,6 +113,7 @@ VALID_IMAGES: Final = [
"raspberrypi3", "raspberrypi3",
"raspberrypi4-64", "raspberrypi4-64",
"raspberrypi4", "raspberrypi4",
"raspberrypi5-64",
"tinker", "tinker",
] ]

View File

@ -21,15 +21,15 @@
"universal_silabs_flasher" "universal_silabs_flasher"
], ],
"requirements": [ "requirements": [
"bellows==0.37.3", "bellows==0.37.4",
"pyserial==3.5", "pyserial==3.5",
"pyserial-asyncio==0.6", "pyserial-asyncio==0.6",
"zha-quirks==0.0.107", "zha-quirks==0.0.108",
"zigpy-deconz==0.22.2", "zigpy-deconz==0.22.3",
"zigpy==0.60.1", "zigpy==0.60.2",
"zigpy-xbee==0.20.1", "zigpy-xbee==0.20.1",
"zigpy-zigate==0.12.0", "zigpy-zigate==0.12.0",
"zigpy-znp==0.12.0", "zigpy-znp==0.12.1",
"universal-silabs-flasher==0.0.15", "universal-silabs-flasher==0.0.15",
"pyserial-asyncio-fast==0.11" "pyserial-asyncio-fast==0.11"
], ],

View File

@ -7,7 +7,7 @@ from typing import Final
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023 MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 12 MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "3" PATCH_VERSION: Final = "4"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)

8
machine/raspberrypi5-64 Normal file
View File

@ -0,0 +1,8 @@
ARG \
BUILD_FROM
FROM $BUILD_FROM
RUN apk --no-cache add \
raspberrypi-userland \
raspberrypi-userland-libs

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2023.12.3" version = "2023.12.4"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"

View File

@ -191,7 +191,7 @@ aioairq==0.3.1
aioairzone-cloud==0.3.6 aioairzone-cloud==0.3.6
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.6.9 aioairzone==0.7.2
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
aioambient==2023.04.0 aioambient==2023.04.0
@ -318,7 +318,7 @@ aioopenexchangerates==0.4.0
aiopegelonline==0.0.6 aiopegelonline==0.0.6
# homeassistant.components.acmeda # homeassistant.components.acmeda
aiopulse==0.4.3 aiopulse==0.4.4
# homeassistant.components.purpleair # homeassistant.components.purpleair
aiopurpleair==2022.12.1 aiopurpleair==2022.12.1
@ -523,7 +523,7 @@ beautifulsoup4==4.12.2
# beewi-smartclim==0.0.10 # beewi-smartclim==0.0.10
# homeassistant.components.zha # homeassistant.components.zha
bellows==0.37.3 bellows==0.37.4
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer-connected[china]==0.14.6 bimmer-connected[china]==0.14.6
@ -541,7 +541,7 @@ bleak==0.21.1
blebox-uniapi==2.2.0 blebox-uniapi==2.2.0
# homeassistant.components.blink # homeassistant.components.blink
blinkpy==0.22.3 blinkpy==0.22.4
# homeassistant.components.bitcoin # homeassistant.components.bitcoin
blockchain==1.4.4 blockchain==1.4.4
@ -681,7 +681,7 @@ demetriek==0.4.0
denonavr==0.11.4 denonavr==0.11.4
# homeassistant.components.devialet # homeassistant.components.devialet
devialet==1.4.3 devialet==1.4.5
# homeassistant.components.devolo_home_control # homeassistant.components.devolo_home_control
devolo-home-control-api==0.18.2 devolo-home-control-api==0.18.2
@ -1162,7 +1162,7 @@ librouteros==3.2.0
libsoundtouch==0.8 libsoundtouch==0.8
# homeassistant.components.life360 # homeassistant.components.life360
life360==6.0.0 life360==6.0.1
# homeassistant.components.osramlightify # homeassistant.components.osramlightify
lightify==1.0.7.3 lightify==1.0.7.3
@ -1261,7 +1261,7 @@ moehlenhoff-alpha2==1.3.0
mopeka-iot-ble==0.5.0 mopeka-iot-ble==0.5.0
# homeassistant.components.motion_blinds # homeassistant.components.motion_blinds
motionblinds==0.6.18 motionblinds==0.6.19
# homeassistant.components.motioneye # homeassistant.components.motioneye
motioneye-client==0.3.14 motioneye-client==0.3.14
@ -1619,7 +1619,7 @@ pyasuswrt==0.1.20
pyatag==0.3.5.3 pyatag==0.3.5.3
# homeassistant.components.netatmo # homeassistant.components.netatmo
pyatmo==7.6.0 pyatmo==8.0.1
# homeassistant.components.apple_tv # homeassistant.components.apple_tv
pyatv==0.14.3 pyatv==0.14.3
@ -1715,7 +1715,7 @@ pyedimax==0.2.1
pyefergy==22.1.1 pyefergy==22.1.1
# homeassistant.components.enphase_envoy # homeassistant.components.enphase_envoy
pyenphase==1.14.3 pyenphase==1.15.2
# homeassistant.components.envisalink # homeassistant.components.envisalink
pyenvisalink==4.6 pyenvisalink==4.6
@ -2245,7 +2245,7 @@ pytrydan==0.4.0
pyudev==0.23.2 pyudev==0.23.2
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
pyunifiprotect==4.22.0 pyunifiprotect==4.22.3
# homeassistant.components.uptimerobot # homeassistant.components.uptimerobot
pyuptimerobot==22.2.0 pyuptimerobot==22.2.0
@ -2338,7 +2338,7 @@ renault-api==0.2.1
renson-endura-delta==1.6.0 renson-endura-delta==1.6.0
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.8.2 reolink-aio==0.8.4
# homeassistant.components.idteck_prox # homeassistant.components.idteck_prox
rfk101py==0.0.1 rfk101py==0.0.1
@ -2347,7 +2347,7 @@ rfk101py==0.0.1
rflink==0.0.65 rflink==0.0.65
# homeassistant.components.ring # homeassistant.components.ring
ring-doorbell[listen]==0.8.3 ring-doorbell[listen]==0.8.5
# homeassistant.components.fleetgo # homeassistant.components.fleetgo
ritassist==0.9.2 ritassist==0.9.2
@ -2537,7 +2537,7 @@ subarulink==0.7.9
sunwatcher==0.2.1 sunwatcher==0.2.1
# homeassistant.components.surepetcare # homeassistant.components.surepetcare
surepy==0.8.0 surepy==0.9.0
# homeassistant.components.swiss_hydrological_data # homeassistant.components.swiss_hydrological_data
swisshydrodata==0.1.0 swisshydrodata==0.1.0
@ -2816,7 +2816,7 @@ zeroconf==0.128.5
zeversolar==0.3.1 zeversolar==0.3.1
# homeassistant.components.zha # homeassistant.components.zha
zha-quirks==0.0.107 zha-quirks==0.0.108
# homeassistant.components.zhong_hong # homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.9 zhong-hong-hvac==1.0.9
@ -2825,7 +2825,7 @@ zhong-hong-hvac==1.0.9
ziggo-mediabox-xl==1.1.0 ziggo-mediabox-xl==1.1.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy-deconz==0.22.2 zigpy-deconz==0.22.3
# homeassistant.components.zha # homeassistant.components.zha
zigpy-xbee==0.20.1 zigpy-xbee==0.20.1
@ -2834,10 +2834,10 @@ zigpy-xbee==0.20.1
zigpy-zigate==0.12.0 zigpy-zigate==0.12.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy-znp==0.12.0 zigpy-znp==0.12.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy==0.60.1 zigpy==0.60.2
# homeassistant.components.zoneminder # homeassistant.components.zoneminder
zm-py==0.5.2 zm-py==0.5.2

View File

@ -170,7 +170,7 @@ aioairq==0.3.1
aioairzone-cloud==0.3.6 aioairzone-cloud==0.3.6
# homeassistant.components.airzone # homeassistant.components.airzone
aioairzone==0.6.9 aioairzone==0.7.2
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
aioambient==2023.04.0 aioambient==2023.04.0
@ -291,7 +291,7 @@ aioopenexchangerates==0.4.0
aiopegelonline==0.0.6 aiopegelonline==0.0.6
# homeassistant.components.acmeda # homeassistant.components.acmeda
aiopulse==0.4.3 aiopulse==0.4.4
# homeassistant.components.purpleair # homeassistant.components.purpleair
aiopurpleair==2022.12.1 aiopurpleair==2022.12.1
@ -445,7 +445,7 @@ base36==0.1.1
beautifulsoup4==4.12.2 beautifulsoup4==4.12.2
# homeassistant.components.zha # homeassistant.components.zha
bellows==0.37.3 bellows==0.37.4
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer-connected[china]==0.14.6 bimmer-connected[china]==0.14.6
@ -460,7 +460,7 @@ bleak==0.21.1
blebox-uniapi==2.2.0 blebox-uniapi==2.2.0
# homeassistant.components.blink # homeassistant.components.blink
blinkpy==0.22.3 blinkpy==0.22.4
# homeassistant.components.bluemaestro # homeassistant.components.bluemaestro
bluemaestro-ble==0.2.3 bluemaestro-ble==0.2.3
@ -556,7 +556,7 @@ demetriek==0.4.0
denonavr==0.11.4 denonavr==0.11.4
# homeassistant.components.devialet # homeassistant.components.devialet
devialet==1.4.3 devialet==1.4.5
# homeassistant.components.devolo_home_control # homeassistant.components.devolo_home_control
devolo-home-control-api==0.18.2 devolo-home-control-api==0.18.2
@ -913,7 +913,7 @@ librouteros==3.2.0
libsoundtouch==0.8 libsoundtouch==0.8
# homeassistant.components.life360 # homeassistant.components.life360
life360==6.0.0 life360==6.0.1
# homeassistant.components.linear_garage_door # homeassistant.components.linear_garage_door
linear-garage-door==0.2.7 linear-garage-door==0.2.7
@ -985,7 +985,7 @@ moehlenhoff-alpha2==1.3.0
mopeka-iot-ble==0.5.0 mopeka-iot-ble==0.5.0
# homeassistant.components.motion_blinds # homeassistant.components.motion_blinds
motionblinds==0.6.18 motionblinds==0.6.19
# homeassistant.components.motioneye # homeassistant.components.motioneye
motioneye-client==0.3.14 motioneye-client==0.3.14
@ -1235,7 +1235,7 @@ pyasuswrt==0.1.20
pyatag==0.3.5.3 pyatag==0.3.5.3
# homeassistant.components.netatmo # homeassistant.components.netatmo
pyatmo==7.6.0 pyatmo==8.0.1
# homeassistant.components.apple_tv # homeassistant.components.apple_tv
pyatv==0.14.3 pyatv==0.14.3
@ -1295,7 +1295,7 @@ pyeconet==0.1.22
pyefergy==22.1.1 pyefergy==22.1.1
# homeassistant.components.enphase_envoy # homeassistant.components.enphase_envoy
pyenphase==1.14.3 pyenphase==1.15.2
# homeassistant.components.everlights # homeassistant.components.everlights
pyeverlights==0.1.0 pyeverlights==0.1.0
@ -1681,7 +1681,7 @@ pytrydan==0.4.0
pyudev==0.23.2 pyudev==0.23.2
# homeassistant.components.unifiprotect # homeassistant.components.unifiprotect
pyunifiprotect==4.22.0 pyunifiprotect==4.22.3
# homeassistant.components.uptimerobot # homeassistant.components.uptimerobot
pyuptimerobot==22.2.0 pyuptimerobot==22.2.0
@ -1750,13 +1750,13 @@ renault-api==0.2.1
renson-endura-delta==1.6.0 renson-endura-delta==1.6.0
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.8.2 reolink-aio==0.8.4
# homeassistant.components.rflink # homeassistant.components.rflink
rflink==0.0.65 rflink==0.0.65
# homeassistant.components.ring # homeassistant.components.ring
ring-doorbell[listen]==0.8.3 ring-doorbell[listen]==0.8.5
# homeassistant.components.roku # homeassistant.components.roku
rokuecp==0.18.1 rokuecp==0.18.1
@ -1898,7 +1898,7 @@ subarulink==0.7.9
sunwatcher==0.2.1 sunwatcher==0.2.1
# homeassistant.components.surepetcare # homeassistant.components.surepetcare
surepy==0.8.0 surepy==0.9.0
# homeassistant.components.switchbot_cloud # homeassistant.components.switchbot_cloud
switchbot-api==1.2.1 switchbot-api==1.2.1
@ -2111,10 +2111,10 @@ zeroconf==0.128.5
zeversolar==0.3.1 zeversolar==0.3.1
# homeassistant.components.zha # homeassistant.components.zha
zha-quirks==0.0.107 zha-quirks==0.0.108
# homeassistant.components.zha # homeassistant.components.zha
zigpy-deconz==0.22.2 zigpy-deconz==0.22.3
# homeassistant.components.zha # homeassistant.components.zha
zigpy-xbee==0.20.1 zigpy-xbee==0.20.1
@ -2123,10 +2123,10 @@ zigpy-xbee==0.20.1
zigpy-zigate==0.12.0 zigpy-zigate==0.12.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy-znp==0.12.0 zigpy-znp==0.12.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy==0.60.1 zigpy==0.60.2
# homeassistant.components.zwave_js # homeassistant.components.zwave_js
zwave-js-server-python==0.54.0 zwave-js-server-python==0.54.0

View File

@ -188,7 +188,7 @@
'coldStages': 0, 'coldStages': 0,
'coolmaxtemp': 90, 'coolmaxtemp': 90,
'coolmintemp': 64, 'coolmintemp': 64,
'coolsetpoint': 73, 'coolsetpoint': 77,
'errors': list([ 'errors': list([
]), ]),
'floor_demand': 0, 'floor_demand': 0,
@ -196,7 +196,7 @@
'heatStages': 0, 'heatStages': 0,
'heatmaxtemp': 86, 'heatmaxtemp': 86,
'heatmintemp': 50, 'heatmintemp': 50,
'heatsetpoint': 77, 'heatsetpoint': 73,
'humidity': 0, 'humidity': 0,
'maxTemp': 90, 'maxTemp': 90,
'minTemp': 64, 'minTemp': 64,
@ -601,7 +601,7 @@
1, 1,
]), ]),
'demand': False, 'demand': False,
'double-set-point': True, 'double-set-point': False,
'full-name': 'Airzone [2:1] Airzone 2:1', 'full-name': 'Airzone [2:1] Airzone 2:1',
'heat-stage': 1, 'heat-stage': 1,
'heat-stages': list([ 'heat-stages': list([
@ -644,7 +644,7 @@
'cold-stage': 0, 'cold-stage': 0,
'cool-temp-max': 90.0, 'cool-temp-max': 90.0,
'cool-temp-min': 64.0, 'cool-temp-min': 64.0,
'cool-temp-set': 73.0, 'cool-temp-set': 77.0,
'demand': True, 'demand': True,
'double-set-point': True, 'double-set-point': True,
'floor-demand': False, 'floor-demand': False,
@ -652,7 +652,7 @@
'heat-stage': 0, 'heat-stage': 0,
'heat-temp-max': 86.0, 'heat-temp-max': 86.0,
'heat-temp-min': 50.0, 'heat-temp-min': 50.0,
'heat-temp-set': 77.0, 'heat-temp-set': 73.0,
'id': 1, 'id': 1,
'master': True, 'master': True,
'mode': 7, 'mode': 7,

View File

@ -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_MAX_TEMP) == 32.2
assert state.attributes.get(ATTR_MIN_TEMP) == 17.8 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_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 = copy.deepcopy(HVAC_MOCK)
HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MAX_TEMP] = 25 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_SYSTEM_ID: 3,
API_ZONE_ID: 1, API_ZONE_ID: 1,
API_COOL_SET_POINT: 68.0, API_COOL_SET_POINT: 77.0,
API_HEAT_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") state = hass.states.get("climate.dkn_plus")
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 20.0 assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 25.0
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 25.0 assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 20.0

View File

@ -245,10 +245,10 @@ HVAC_MOCK = {
API_ZONE_ID: 1, API_ZONE_ID: 1,
API_NAME: "DKN Plus", API_NAME: "DKN Plus",
API_ON: 1, API_ON: 1,
API_COOL_SET_POINT: 73, API_COOL_SET_POINT: 77,
API_COOL_MAX_TEMP: 90, API_COOL_MAX_TEMP: 90,
API_COOL_MIN_TEMP: 64, API_COOL_MIN_TEMP: 64,
API_HEAT_SET_POINT: 77, API_HEAT_SET_POINT: 73,
API_HEAT_MAX_TEMP: 86, API_HEAT_MAX_TEMP: 86,
API_HEAT_MIN_TEMP: 50, API_HEAT_MIN_TEMP: 50,
API_MAX_TEMP: 90, API_MAX_TEMP: 90,

View File

@ -87,6 +87,7 @@ def mock_config_fixture():
"device_id": "Home Assistant", "device_id": "Home Assistant",
"uid": "BlinkCamera_e1233333e2-0909-09cd-777a-123456789012", "uid": "BlinkCamera_e1233333e2-0909-09cd-777a-123456789012",
"token": "A_token", "token": "A_token",
"unique_id": "an_email@email.com",
"host": "u034.immedia-semi.com", "host": "u034.immedia-semi.com",
"region_id": "u034", "region_id": "u034",
"client_id": 123456, "client_id": 123456,

View File

@ -34,6 +34,7 @@
'region_id': 'u034', 'region_id': 'u034',
'token': '**REDACTED**', 'token': '**REDACTED**',
'uid': 'BlinkCamera_e1233333e2-0909-09cd-777a-123456789012', 'uid': 'BlinkCamera_e1233333e2-0909-09cd-777a-123456789012',
'unique_id': '**REDACTED**',
'username': '**REDACTED**', 'username': '**REDACTED**',
}), }),
'disabled_by': None, 'disabled_by': None,

View File

@ -4,12 +4,13 @@ from http import HTTPStatus
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from requests.exceptions import ReadTimeout
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant.components.fritz.const import DOMAIN from homeassistant.components.fritz.const import DOMAIN
from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN
from homeassistant.config_entries import ConfigEntryState 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.core import HomeAssistant
from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
from homeassistant.setup import async_setup_component 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 != resp_body_new
assert resp_body_new == snapshot 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

View File

@ -475,22 +475,12 @@
"last_setup": 1558709954, "last_setup": 1558709954,
"data_type": ["Temperature", "Humidity"], "data_type": ["Temperature", "Humidity"],
"battery_percent": 27, "battery_percent": 27,
"reachable": true, "reachable": false,
"firmware": 50, "firmware": 50,
"last_message": 1644582699, "last_message": 1644582699,
"last_seen": 1644582699, "last_seen": 1644582699,
"rf_status": 68, "rf_status": 68,
"battery_vp": 4678, "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"
}
}, },
{ {
"_id": "12:34:56:80:c1:ea", "_id": "12:34:56:80:c1:ea",

View File

@ -561,26 +561,28 @@
'access_doorbell', 'access_doorbell',
'access_presence', 'access_presence',
'read_bubendorff', 'read_bubendorff',
'read_bfi',
'read_camera', 'read_camera',
'read_carbonmonoxidedetector', 'read_carbonmonoxidedetector',
'read_doorbell', 'read_doorbell',
'read_homecoach', 'read_homecoach',
'read_magellan', 'read_magellan',
'read_mhs1',
'read_mx', 'read_mx',
'read_presence', 'read_presence',
'read_smarther', 'read_smarther',
'read_smokedetector', 'read_smokedetector',
'read_station', 'read_station',
'read_thermostat', 'read_thermostat',
'read_mhs1',
'write_bubendorff', 'write_bubendorff',
'write_bfi',
'write_camera', 'write_camera',
'write_magellan', 'write_magellan',
'write_mhs1',
'write_mx', 'write_mx',
'write_presence', 'write_presence',
'write_smarther', 'write_smarther',
'write_thermostat', 'write_thermostat',
'write_mhs1',
]), ]),
'type': 'Bearer', 'type': 'Bearer',
}), }),

View File

@ -10,8 +10,8 @@ from homeassistant.helpers import entity_registry as er
from .common import TEST_TIME, selected_platforms from .common import TEST_TIME, selected_platforms
async def test_weather_sensor(hass: HomeAssistant, config_entry, netatmo_auth) -> None: async def test_indoor_sensor(hass: HomeAssistant, config_entry, netatmo_auth) -> None:
"""Test weather sensor setup.""" """Test indoor sensor setup."""
with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]):
assert await hass.config_entries.async_setup(config_entry.entry_id) 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" 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( async def test_public_weather_sensor(
hass: HomeAssistant, config_entry, netatmo_auth hass: HomeAssistant, config_entry, netatmo_auth
) -> None: ) -> None:

View File

@ -1,6 +1,6 @@
"""Tests for the OurGroceries integration.""" """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.""" """Convert a list of items into a shopping list."""
return {"list": {"items": items}} return {"list": {"versionId": version_id, "items": items}}

View File

@ -46,7 +46,7 @@ def mock_ourgroceries(items: list[dict]) -> AsyncMock:
og = AsyncMock() og = AsyncMock()
og.login.return_value = True og.login.return_value = True
og.get_my_lists.return_value = { 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) og.get_list_items.return_value = items_to_shopping_list(items)
return og return og

View File

@ -17,6 +17,10 @@ from . import items_to_shopping_list
from tests.common import async_fire_time_changed 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( @pytest.mark.parametrize(
("items", "expected_state"), ("items", "expected_state"),
[ [
@ -57,8 +61,10 @@ async def test_add_todo_list_item(
ourgroceries.add_item_to_list = AsyncMock() ourgroceries.add_item_to_list = AsyncMock()
# Fake API response when state is refreshed after create # Fake API response when state is refreshed after create
_mock_version_id(ourgroceries, 2)
ourgroceries.get_list_items.return_value = items_to_shopping_list( 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( await hass.services.async_call(
@ -95,6 +101,7 @@ async def test_update_todo_item_status(
ourgroceries.toggle_item_crossed_off = AsyncMock() ourgroceries.toggle_item_crossed_off = AsyncMock()
# Fake API response when state is refreshed after crossing off # 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( ourgroceries.get_list_items.return_value = items_to_shopping_list(
[{"id": "12345", "name": "Soda", "crossedOffAt": 1699107501}] [{"id": "12345", "name": "Soda", "crossedOffAt": 1699107501}]
) )
@ -118,6 +125,7 @@ async def test_update_todo_item_status(
assert state.state == "0" assert state.state == "0"
# Fake API response when state is refreshed after reopen # Fake API response when state is refreshed after reopen
_mock_version_id(ourgroceries, 2)
ourgroceries.get_list_items.return_value = items_to_shopping_list( ourgroceries.get_list_items.return_value = items_to_shopping_list(
[{"id": "12345", "name": "Soda"}] [{"id": "12345", "name": "Soda"}]
) )
@ -166,6 +174,7 @@ async def test_update_todo_item_summary(
ourgroceries.change_item_on_list = AsyncMock() ourgroceries.change_item_on_list = AsyncMock()
# Fake API response when state is refreshed update # Fake API response when state is refreshed update
_mock_version_id(ourgroceries, 2)
ourgroceries.get_list_items.return_value = items_to_shopping_list( ourgroceries.get_list_items.return_value = items_to_shopping_list(
[{"id": "12345", "name": "Milk"}] [{"id": "12345", "name": "Milk"}]
) )
@ -204,6 +213,7 @@ async def test_remove_todo_item(
ourgroceries.remove_item_from_list = AsyncMock() ourgroceries.remove_item_from_list = AsyncMock()
# Fake API response when state is refreshed after remove # Fake API response when state is refreshed after remove
_mock_version_id(ourgroceries, 2)
ourgroceries.get_list_items.return_value = items_to_shopping_list([]) ourgroceries.get_list_items.return_value = items_to_shopping_list([])
await hass.services.async_call( await hass.services.async_call(
@ -224,6 +234,25 @@ async def test_remove_todo_item(
assert state.state == "0" 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( @pytest.mark.parametrize(
("exception"), ("exception"),
[ [
@ -242,6 +271,7 @@ async def test_coordinator_error(
state = hass.states.get("todo.test_list") state = hass.states.get("todo.test_list")
assert state.state == "0" assert state.state == "0"
_mock_version_id(ourgroceries, 2)
ourgroceries.get_list_items.side_effect = exception ourgroceries.get_list_items.side_effect = exception
freezer.tick(SCAN_INTERVAL) freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass) async_fire_time_changed(hass)

View File

@ -1,5 +1,4 @@
"""The tests for the REST switch platform.""" """The tests for the REST switch platform."""
import asyncio
from http import HTTPStatus from http import HTTPStatus
import httpx import httpx
@ -84,7 +83,7 @@ async def test_setup_failed_connect(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test setup when connection error occurs.""" """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}} config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}}
assert await async_setup_component(hass, SWITCH_DOMAIN, config) assert await async_setup_component(hass, SWITCH_DOMAIN, config)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -98,7 +97,7 @@ async def test_setup_timeout(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test setup when connection timeout occurs.""" """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}} config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}}
assert await async_setup_component(hass, SWITCH_DOMAIN, config) assert await async_setup_component(hass, SWITCH_DOMAIN, config)
await hass.async_block_till_done() 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.""" """Test turn_on when timeout occurs."""
await _async_setup_test_switch(hass) 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( await hass.services.async_call(
SWITCH_DOMAIN, SWITCH_DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,
@ -364,7 +363,7 @@ async def test_turn_off_timeout(hass: HomeAssistant) -> None:
"""Test turn_off when timeout occurs.""" """Test turn_off when timeout occurs."""
await _async_setup_test_switch(hass) 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( await hass.services.async_call(
SWITCH_DOMAIN, SWITCH_DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
@ -417,7 +416,7 @@ async def test_update_timeout(hass: HomeAssistant) -> None:
"""Test update when timeout occurs.""" """Test update when timeout occurs."""
await _async_setup_test_switch(hass) 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) async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -1038,6 +1038,7 @@ async def test_add_item_intent(
assert len(entity1.items) == 1 assert len(entity1.items) == 1
assert len(entity2.items) == 0 assert len(entity2.items) == 0
assert entity1.items[0].summary == "beer" assert entity1.items[0].summary == "beer"
assert entity1.items[0].status == TodoItemStatus.NEEDS_ACTION
entity1.items.clear() entity1.items.clear()
# Add to second list # Add to second list
@ -1052,6 +1053,7 @@ async def test_add_item_intent(
assert len(entity1.items) == 0 assert len(entity1.items) == 0
assert len(entity2.items) == 1 assert len(entity2.items) == 1
assert entity2.items[0].summary == "cheese" assert entity2.items[0].summary == "cheese"
assert entity2.items[0].status == TodoItemStatus.NEEDS_ACTION
# List name is case insensitive # List name is case insensitive
response = await intent.async_handle( response = await intent.async_handle(
@ -1065,6 +1067,7 @@ async def test_add_item_intent(
assert len(entity1.items) == 0 assert len(entity1.items) == 0
assert len(entity2.items) == 2 assert len(entity2.items) == 2
assert entity2.items[1].summary == "wine" assert entity2.items[1].summary == "wine"
assert entity2.items[1].status == TodoItemStatus.NEEDS_ACTION
# Missing list # Missing list
with pytest.raises(intent.IntentHandleError): with pytest.raises(intent.IntentHandleError):