mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 18:27:09 +00:00
2023.12.4 (#106460)
This commit is contained in:
commit
ae80d576bf
5
.github/workflows/builder.yml
vendored
5
.github/workflows/builder.yml
vendored
@ -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 \
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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."]
|
||||
}
|
||||
|
@ -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."
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -270,6 +270,7 @@ HARDWARE_INTEGRATIONS = {
|
||||
"rpi3-64": "raspberry_pi",
|
||||
"rpi4": "raspberry_pi",
|
||||
"rpi4-64": "raspberry_pi",
|
||||
"rpi5-64": "raspberry_pi",
|
||||
"yellow": "homeassistant_yellow",
|
||||
}
|
||||
|
||||
|
@ -507,6 +507,7 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
except (
|
||||
AuthError,
|
||||
ClientConnectionError,
|
||||
AscConnectionError,
|
||||
asyncio.TimeoutError,
|
||||
):
|
||||
self._retry += 1
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyatmo"],
|
||||
"requirements": ["pyatmo==7.6.0"]
|
||||
"requirements": ["pyatmo==8.0.1"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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]]:
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
]
|
||||
|
||||
|
@ -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"
|
||||
],
|
||||
|
@ -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)
|
||||
|
8
machine/raspberrypi5-64
Normal file
8
machine/raspberrypi5-64
Normal file
@ -0,0 +1,8 @@
|
||||
ARG \
|
||||
BUILD_FROM
|
||||
|
||||
FROM $BUILD_FROM
|
||||
|
||||
RUN apk --no-cache add \
|
||||
raspberrypi-userland \
|
||||
raspberrypi-userland-libs
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -34,6 +34,7 @@
|
||||
'region_id': 'u034',
|
||||
'token': '**REDACTED**',
|
||||
'uid': 'BlinkCamera_e1233333e2-0909-09cd-777a-123456789012',
|
||||
'unique_id': '**REDACTED**',
|
||||
'username': '**REDACTED**',
|
||||
}),
|
||||
'disabled_by': None,
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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',
|
||||
}),
|
||||
|
@ -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:
|
||||
|
@ -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}}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user