* Streamline Notion config entry updates (refresh token and user ID) (#112832)

* Bump aioautomower to 2024.3.2 (#113162)

* Bump aioautomower to 2024.3.3 (#113430)

* Check for EA release channel for UniFi Protect (#113432)

Co-authored-by: J. Nick Koston <nick@koston.org>

* Bump `pysnmp-lextudio` to version `6.0.11` (#113463)

* Tado fix water heater (#113464)

Co-authored-by: Joostlek <joostlek@outlook.com>

* Bump aiodhcpwatcher to 0.8.2 (#113466)

* Bump axis to v55 (#113479)

* Bump croniter to 2.0.2 (#113494)

* Revert setting communication delay in Risco init (#113497)

* Bump pyrisco to 0.5.10 (#113505)

* Fix missing context when running script from template entity (#113523)

Co-authored-by: J. Nick Koston <nick@koston.org>

* Bump ical to 7.0.3 to fix local-todo persisted with invalid DTSTART values (#113526)

* Fix Airthings BLE illuminance sensor name (#113560)

* Ignore Shelly block update with cfgChanged None (#113587)

* Catch `TimeoutError` in `Brother` config flow (#113593)

* Catch TimeoutError in Brother config flow

* Update tests

* Remove unnecessary parentheses

---------

Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>

* Bump axis to v56 (#113608)

* Bump pyunifiprotect to 5.0.1 (#113630)

* Bump pyunifiprotect to 5.0.2 (#113651)

* Add removal condition to Shelly battery sensor (#113703)

Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>

* Bump aioraven to 0.5.2 (#113714)

* Fix unknown values in onewire (#113731)

* Fix unknown values in onewire

* Update tests

* Bump pymodbus v3.6.6 (#113796)

* Catch API errors in cast media_player service handlers (#113839)

* Catch API errors in cast media_player service handlers

* Remove left over debug code

* Fix wrapping of coroutine function with api_error

* Bump pychromecast to 14.0.1 (#113841)

* Fix startup race in cast (#113843)

* Redact the area of traccar server geofences (#113861)

* Bump pytedee_async to 0.2.17 (#113933)

* Bump axis to v57 (#113952)

* Bump version to 2024.3.2

---------

Co-authored-by: Aaron Bach <bachya1208@gmail.com>
Co-authored-by: Thomas55555 <59625598+Thomas55555@users.noreply.github.com>
Co-authored-by: Christopher Bailey <cbailey@mort.is>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Lex Li <425130+lextm@users.noreply.github.com>
Co-authored-by: Erwin Douna <e.douna@gmail.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>
Co-authored-by: Diogo Gomes <diogogomes@gmail.com>
Co-authored-by: On Freund <onfreund@gmail.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Allen Porter <allen@thebends.org>
Co-authored-by: Shay Levy <levyshay1@gmail.com>
Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
Co-authored-by: Scott K Logan <logans@cottsay.net>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: jan iversen <jancasacondor@gmail.com>
Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev>
Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com>
This commit is contained in:
Paulus Schoutsen 2024-03-21 21:57:36 -04:00 committed by GitHub
parent f7972ce9b2
commit f10d924e8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 288 additions and 196 deletions

View File

@ -105,6 +105,7 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
),
"illuminance": SensorEntityDescription(
key="illuminance",
translation_key="illuminance",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),

View File

@ -33,6 +33,9 @@
},
"radon_longterm_level": {
"name": "Radon longterm level"
},
"illuminance": {
"name": "[%key:component::sensor::entity_component::illuminance::name%]"
}
}
}

View File

@ -26,7 +26,7 @@
"iot_class": "local_push",
"loggers": ["axis"],
"quality_scale": "platinum",
"requirements": ["axis==54"],
"requirements": ["axis==57"],
"ssdp": [
{
"manufacturer": "AXIS"

View File

@ -58,7 +58,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry(title=title, data=user_input)
except InvalidHost:
errors[CONF_HOST] = "wrong_host"
except ConnectionError:
except (ConnectionError, TimeoutError):
errors["base"] = "cannot_connect"
except SnmpError:
errors["base"] = "snmp_error"
@ -88,7 +88,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.brother.async_update()
except UnsupportedModelError:
return self.async_abort(reason="unsupported_model")
except (ConnectionError, SnmpError):
except (ConnectionError, SnmpError, TimeoutError):
return self.async_abort(reason="cannot_connect")
# Check if already configured

View File

@ -24,9 +24,9 @@ PLATFORMS = [Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Cast from a config entry."""
hass.data[DOMAIN] = {"cast_platform": {}, "unknown_models": {}}
await home_assistant_cast.async_setup_ha_cast(hass, entry)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
hass.data[DOMAIN] = {"cast_platform": {}, "unknown_models": {}}
await async_process_integration_platforms(hass, DOMAIN, _register_cast_platform)
return True

View File

@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/cast",
"iot_class": "local_polling",
"loggers": ["casttube", "pychromecast"],
"requirements": ["PyChromecast==14.0.0"],
"requirements": ["PyChromecast==14.0.1"],
"zeroconf": ["_googlecast._tcp.local."]
}

View File

@ -4,9 +4,10 @@ from __future__ import annotations
from collections.abc import Callable
from contextlib import suppress
from datetime import datetime
from functools import wraps
import json
import logging
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar
import pychromecast
from pychromecast.controllers.homeassistant import HomeAssistantController
@ -18,6 +19,7 @@ from pychromecast.controllers.media import (
)
from pychromecast.controllers.multizone import MultizoneManager
from pychromecast.controllers.receiver import VOLUME_CONTROL_TYPE_FIXED
from pychromecast.error import PyChromecastError
from pychromecast.quick_play import quick_play
from pychromecast.socket_client import (
CONNECTION_STATUS_CONNECTED,
@ -83,6 +85,34 @@ APP_IDS_UNRELIABLE_MEDIA_INFO = ("Netflix",)
CAST_SPLASH = "https://www.home-assistant.io/images/cast/splash.png"
_CastDeviceT = TypeVar("_CastDeviceT", bound="CastDevice")
_R = TypeVar("_R")
_P = ParamSpec("_P")
_FuncType = Callable[Concatenate[_CastDeviceT, _P], _R]
_ReturnFuncType = Callable[Concatenate[_CastDeviceT, _P], _R]
def api_error(
func: _FuncType[_CastDeviceT, _P, _R],
) -> _ReturnFuncType[_CastDeviceT, _P, _R]:
"""Handle PyChromecastError and reraise a HomeAssistantError."""
@wraps(func)
def wrapper(self: _CastDeviceT, *args: _P.args, **kwargs: _P.kwargs) -> _R:
"""Wrap a CastDevice method."""
try:
return_value = func(self, *args, **kwargs)
except PyChromecastError as err:
raise HomeAssistantError(
f"{self.__class__.__name__}.{func.__name__} Failed: {err}"
) from err
return return_value
return wrapper
@callback
def _async_create_cast_device(hass: HomeAssistant, info: ChromecastInfo):
"""Create a CastDevice entity or dynamic group from the chromecast object.
@ -476,6 +506,21 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
return media_controller
@api_error
def _quick_play(self, app_name: str, data: dict[str, Any]) -> None:
"""Launch the app `app_name` and start playing media defined by `data`."""
quick_play(self._get_chromecast(), app_name, data)
@api_error
def _quit_app(self) -> None:
"""Quit the currently running app."""
self._get_chromecast().quit_app()
@api_error
def _start_app(self, app_id: str) -> None:
"""Start an app."""
self._get_chromecast().start_app(app_id)
def turn_on(self) -> None:
"""Turn on the cast device."""
@ -486,52 +531,61 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
if chromecast.app_id is not None:
# Quit the previous app before starting splash screen or media player
chromecast.quit_app()
self._quit_app()
# The only way we can turn the Chromecast is on is by launching an app
if chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST:
app_data = {"media_id": CAST_SPLASH, "media_type": "image/png"}
quick_play(chromecast, "default_media_receiver", app_data)
self._quick_play("default_media_receiver", app_data)
else:
chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER)
self._start_app(pychromecast.config.APP_MEDIA_RECEIVER)
@api_error
def turn_off(self) -> None:
"""Turn off the cast device."""
self._get_chromecast().quit_app()
@api_error
def mute_volume(self, mute: bool) -> None:
"""Mute the volume."""
self._get_chromecast().set_volume_muted(mute)
@api_error
def set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1."""
self._get_chromecast().set_volume(volume)
@api_error
def media_play(self) -> None:
"""Send play command."""
media_controller = self._media_controller()
media_controller.play()
@api_error
def media_pause(self) -> None:
"""Send pause command."""
media_controller = self._media_controller()
media_controller.pause()
@api_error
def media_stop(self) -> None:
"""Send stop command."""
media_controller = self._media_controller()
media_controller.stop()
@api_error
def media_previous_track(self) -> None:
"""Send previous track command."""
media_controller = self._media_controller()
media_controller.queue_prev()
@api_error
def media_next_track(self) -> None:
"""Send next track command."""
media_controller = self._media_controller()
media_controller.queue_next()
@api_error
def media_seek(self, position: float) -> None:
"""Seek the media to a specific location."""
media_controller = self._media_controller()
@ -644,7 +698,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
if "app_id" in app_data:
app_id = app_data.pop("app_id")
_LOGGER.info("Starting Cast app by ID %s", app_id)
await self.hass.async_add_executor_job(chromecast.start_app, app_id)
await self.hass.async_add_executor_job(self._start_app, app_id)
if app_data:
_LOGGER.warning(
"Extra keys %s were ignored. Please use app_name to cast media",
@ -655,7 +709,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
app_name = app_data.pop("app_name")
try:
await self.hass.async_add_executor_job(
quick_play, chromecast, app_name, app_data
self._quick_play, app_name, app_data
)
except NotImplementedError:
_LOGGER.error("App %s not supported", app_name)
@ -729,7 +783,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
app_data,
)
await self.hass.async_add_executor_job(
quick_play, chromecast, "default_media_receiver", app_data
self._quick_play, "default_media_receiver", app_data
)
def _media_status(self):

View File

@ -15,7 +15,7 @@
],
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==0.8.1",
"aiodhcpwatcher==0.8.2",
"aiodiscover==1.6.1",
"cached_ipaddress==0.3.0"
]

View File

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/calendar.google",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==7.0.1"]
"requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==7.0.3"]
}

View File

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower",
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"requirements": ["aioautomower==2024.3.0"]
"requirements": ["aioautomower==2024.3.3"]
}

View File

@ -69,6 +69,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
exists_fn=lambda data: data.statistics.total_charging_time is not None,
value_fn=lambda data: data.statistics.total_charging_time,
),
AutomowerSensorEntityDescription(
@ -79,6 +80,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
exists_fn=lambda data: data.statistics.total_cutting_time is not None,
value_fn=lambda data: data.statistics.total_cutting_time,
),
AutomowerSensorEntityDescription(
@ -89,6 +91,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
exists_fn=lambda data: data.statistics.total_running_time is not None,
value_fn=lambda data: data.statistics.total_running_time,
),
AutomowerSensorEntityDescription(
@ -99,6 +102,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
exists_fn=lambda data: data.statistics.total_searching_time is not None,
value_fn=lambda data: data.statistics.total_searching_time,
),
AutomowerSensorEntityDescription(
@ -107,6 +111,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
icon="mdi:battery-sync-outline",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL,
exists_fn=lambda data: data.statistics.number_of_charging_cycles is not None,
value_fn=lambda data: data.statistics.number_of_charging_cycles,
),
AutomowerSensorEntityDescription(
@ -115,6 +120,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
icon="mdi:counter",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL,
exists_fn=lambda data: data.statistics.number_of_collisions is not None,
value_fn=lambda data: data.statistics.number_of_collisions,
),
AutomowerSensorEntityDescription(
@ -125,6 +131,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.METERS,
suggested_unit_of_measurement=UnitOfLength.KILOMETERS,
exists_fn=lambda data: data.statistics.total_drive_distance is not None,
value_fn=lambda data: data.statistics.total_drive_distance,
),
AutomowerSensorEntityDescription(

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling",
"loggers": ["ical"],
"requirements": ["ical==7.0.1"]
"requirements": ["ical==7.0.3"]
}

View File

@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_todo",
"iot_class": "local_polling",
"requirements": ["ical==7.0.1"]
"requirements": ["ical==7.0.3"]
}

View File

@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pymodbus"],
"quality_scale": "gold",
"requirements": ["pymodbus==3.6.5"]
"requirements": ["pymodbus==3.6.6"]
}

View File

@ -165,9 +165,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except NotionError as err:
raise ConfigEntryNotReady("Config entry failed to load") from err
# Always update the config entry with the latest refresh token and user UUID:
entry_updates["data"][CONF_REFRESH_TOKEN] = client.refresh_token
entry_updates["data"][CONF_USER_UUID] = client.user_uuid
# Update the Notion user UUID and refresh token if they've changed:
for key, value in (
(CONF_REFRESH_TOKEN, client.refresh_token),
(CONF_USER_UUID, client.user_uuid),
):
if entry.data[key] == value:
continue
entry_updates["data"][key] = value
hass.config_entries.async_update_entry(entry, **entry_updates)
@callback
def async_save_refresh_token(refresh_token: str) -> None:
@ -180,12 +187,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Create a callback to save the refresh token when it changes:
entry.async_on_unload(client.add_refresh_token_callback(async_save_refresh_token))
# Save the client's refresh token if it's different than what we already have:
if (token := client.refresh_token) and token != entry.data[CONF_REFRESH_TOKEN]:
async_save_refresh_token(token)
hass.config_entries.async_update_entry(entry, **entry_updates)
async def async_update() -> NotionData:
"""Get the latest data from the Notion API."""
data = NotionData(hass=hass, entry=entry)

View File

@ -143,6 +143,8 @@ class OneWireBinarySensor(OneWireEntity, BinarySensorEntity):
entity_description: OneWireBinarySensorEntityDescription
@property
def is_on(self) -> bool:
def is_on(self) -> bool | None:
"""Return true if sensor is on."""
if self._state is None:
return None
return bool(self._state)

View File

@ -204,8 +204,10 @@ class OneWireSwitch(OneWireEntity, SwitchEntity):
entity_description: OneWireSwitchEntityDescription
@property
def is_on(self) -> bool:
"""Return true if sensor is on."""
def is_on(self) -> bool | None:
"""Return true if switch is on."""
if self._state is None:
return None
return bool(self._state)
def turn_on(self, **kwargs: Any) -> None:

View File

@ -6,7 +6,7 @@
"dependencies": ["usb"],
"documentation": "https://www.home-assistant.io/integrations/rainforest_raven",
"iot_class": "local_polling",
"requirements": ["aioraven==0.5.1"],
"requirements": ["aioraven==0.5.2"],
"usb": [
{
"vid": "0403",

View File

@ -37,12 +37,10 @@ from homeassistant.helpers.storage import Store
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
CONF_COMMUNICATION_DELAY,
DATA_COORDINATOR,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
EVENTS_COORDINATOR,
MAX_COMMUNICATION_DELAY,
TYPE_LOCAL,
)
@ -85,31 +83,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
data = entry.data
comm_delay = initial_delay = data.get(CONF_COMMUNICATION_DELAY, 0)
risco = RiscoLocal(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN])
while True:
risco = RiscoLocal(
data[CONF_HOST],
data[CONF_PORT],
data[CONF_PIN],
communication_delay=comm_delay,
)
try:
await risco.connect()
except CannotConnectError as error:
if comm_delay >= MAX_COMMUNICATION_DELAY:
raise ConfigEntryNotReady() from error
comm_delay += 1
except UnauthorizedError:
_LOGGER.exception("Failed to login to Risco cloud")
return False
else:
break
if comm_delay > initial_delay:
new_data = data.copy()
new_data[CONF_COMMUNICATION_DELAY] = comm_delay
hass.config_entries.async_update_entry(entry, data=new_data)
try:
await risco.connect()
except CannotConnectError as error:
raise ConfigEntryNotReady() from error
except UnauthorizedError:
_LOGGER.exception("Failed to login to Risco cloud")
return False
async def _error(error: Exception) -> None:
_LOGGER.error("Error in Risco library: %s", error)

View File

@ -7,5 +7,5 @@
"iot_class": "local_push",
"loggers": ["pyrisco"],
"quality_scale": "platinum",
"requirements": ["pyrisco==0.5.8"]
"requirements": ["pyrisco==0.5.10"]
}

View File

@ -216,7 +216,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]):
# Check for input events and config change
cfg_changed = 0
for block in self.device.blocks:
if block.type == "device":
if block.type == "device" and block.cfgChanged is not None:
cfg_changed = block.cfgChanged
# Shelly TRV sends information about changing the configuration for no

View File

@ -941,6 +941,7 @@ RPC_SENSORS: Final = {
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
removal_condition=lambda _config, status, key: (status[key]["battery"] is None),
),
"voltmeter": RpcSensorDescription(
key="voltmeter",

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/snmp",
"iot_class": "local_polling",
"loggers": ["pyasn1", "pysmi", "pysnmp"],
"requirements": ["pysnmp-lextudio==6.0.9"]
"requirements": ["pysnmp-lextudio==6.0.11"]
}

View File

@ -2,7 +2,6 @@
import logging
from typing import Any
import PyTado
import voluptuous as vol
from homeassistant.components.water_heater import (
@ -29,8 +28,6 @@ from .const import (
DATA,
DOMAIN,
SIGNAL_TADO_UPDATE_RECEIVED,
TADO_DEFAULT_MAX_TEMP,
TADO_DEFAULT_MIN_TEMP,
TYPE_HOT_WATER,
)
from .entity import TadoZoneEntity
@ -133,8 +130,8 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
zone_name: str,
zone_id: int,
supports_temperature_control: bool,
min_temp: float | None = None,
max_temp: float | None = None,
min_temp,
max_temp,
) -> None:
"""Initialize of Tado water heater entity."""
self._tado = tado
@ -146,8 +143,8 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
self._device_is_active = False
self._supports_temperature_control = supports_temperature_control
self._min_temperature = min_temp or TADO_DEFAULT_MIN_TEMP
self._max_temperature = max_temp or TADO_DEFAULT_MAX_TEMP
self._min_temperature = min_temp
self._max_temperature = max_temp
self._target_temp: float | None = None
@ -157,7 +154,7 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
self._current_tado_hvac_mode = CONST_MODE_SMART_SCHEDULE
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
self._tado_zone_data: PyTado.TadoZone = {}
self._tado_zone_data: Any = None
async def async_added_to_hass(self) -> None:
"""Register for sensor updates."""

View File

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/tedee",
"iot_class": "local_push",
"loggers": ["pytedee_async"],
"requirements": ["pytedee-async==0.2.16"]
"requirements": ["pytedee-async==0.2.17"]
}

View File

@ -3,7 +3,7 @@ from collections.abc import Callable
import logging
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.core import CoreState, callback
from homeassistant.core import Context, CoreState, callback
from homeassistant.helpers import discovery, trigger as trigger_helper
from homeassistant.helpers.script import Script
from homeassistant.helpers.typing import ConfigType
@ -90,7 +90,10 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator):
)
async def _handle_triggered_with_script(self, run_variables, context=None):
if script_result := await self._script.async_run(run_variables, context):
# Create a context referring to the trigger context.
trigger_context_id = None if context is None else context.id
script_context = Context(parent_id=trigger_context_id)
if script_result := await self._script.async_run(run_variables, script_context):
run_variables = script_result.variables
self._handle_triggered(run_variables, context)

View File

@ -12,7 +12,12 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
from .const import DOMAIN
from .coordinator import TraccarServerCoordinator
TO_REDACT = {CONF_ADDRESS, CONF_LATITUDE, CONF_LONGITUDE}
TO_REDACT = {
CONF_ADDRESS,
CONF_LATITUDE,
CONF_LONGITUDE,
"area", # This is the polygon area of a geofence
}
async def async_get_config_entry_diagnostics(

View File

@ -6,6 +6,7 @@ import logging
from aiohttp.client_exceptions import ServerDisconnectedError
from pyunifiprotect.data import Bootstrap
from pyunifiprotect.data.types import FirmwareReleaseChannel
from pyunifiprotect.exceptions import ClientError, NotAuthorized
# Import the test_util.anonymize module from the pyunifiprotect package
@ -111,19 +112,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, data_service.async_stop)
)
if (
not entry.options.get(CONF_ALLOW_EA, False)
and await nvr_info.get_is_prerelease()
if not entry.options.get(CONF_ALLOW_EA, False) and (
await nvr_info.get_is_prerelease()
or nvr_info.release_channel != FirmwareReleaseChannel.RELEASE
):
ir.async_create_issue(
hass,
DOMAIN,
"ea_warning",
"ea_channel_warning",
is_fixable=True,
is_persistent=True,
learn_more_url="https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access",
severity=IssueSeverity.WARNING,
translation_key="ea_warning",
translation_key="ea_channel_warning",
translation_placeholders={"version": str(nvr_info.version)},
data={"entry_id": entry.entry_id},
)
@ -149,7 +150,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"version": str(nvr_info.version),
},
)
ir.async_delete_issue(hass, DOMAIN, "ea_warning")
ir.async_delete_issue(hass, DOMAIN, "ea_channel_warning")
_LOGGER.exception("Error setting up UniFi Protect integration: %s", err)
raise

View File

@ -24,7 +24,7 @@ CONF_DISABLE_RTSP = "disable_rtsp"
CONF_ALL_UPDATES = "all_updates"
CONF_OVERRIDE_CHOST = "override_connection_host"
CONF_MAX_MEDIA = "max_media"
CONF_ALLOW_EA = "allow_ea"
CONF_ALLOW_EA = "allow_ea_channel"
CONFIG_OPTIONS = [
CONF_ALL_UPDATES,

View File

@ -19,6 +19,7 @@ from pyunifiprotect.data import (
WSSubscriptionMessage,
)
from pyunifiprotect.exceptions import ClientError, NotAuthorized
from pyunifiprotect.utils import log_event
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
@ -41,11 +42,6 @@ from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type
_LOGGER = logging.getLogger(__name__)
ProtectDeviceType = ProtectAdoptableDeviceModel | NVR
SMART_EVENTS = {
EventType.SMART_DETECT,
EventType.SMART_AUDIO_DETECT,
EventType.SMART_DETECT_LINE,
}
@callback
@ -230,26 +226,7 @@ class ProtectData:
# trigger updates for camera that the event references
elif isinstance(obj, Event): # type: ignore[unreachable]
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug("event WS msg: %s", obj.dict())
if obj.type in SMART_EVENTS:
if obj.camera is not None:
if obj.end is None:
_LOGGER.debug(
"%s (%s): New smart detection started for %s (%s)",
obj.camera.name,
obj.camera.mac,
obj.smart_detect_types,
obj.id,
)
else:
_LOGGER.debug(
"%s (%s): Smart detection ended for %s (%s)",
obj.camera.name,
obj.camera.mac,
obj.smart_detect_types,
obj.id,
)
log_event(obj)
if obj.type is EventType.DEVICE_ADOPTED:
if obj.metadata is not None and obj.metadata.device_id is not None:
device = self.api.bootstrap.get_device_from_id(

View File

@ -42,7 +42,7 @@
"iot_class": "local_push",
"loggers": ["pyunifiprotect", "unifi_discovery"],
"quality_scale": "platinum",
"requirements": ["pyunifiprotect==4.23.3", "unifi-discovery==1.1.8"],
"requirements": ["pyunifiprotect==5.0.2", "unifi-discovery==1.1.8"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",

View File

@ -6,6 +6,7 @@ import logging
from typing import cast
from pyunifiprotect import ProtectApiClient
from pyunifiprotect.data.types import FirmwareReleaseChannel
import voluptuous as vol
from homeassistant import data_entry_flow
@ -68,7 +69,7 @@ class EAConfirm(ProtectRepair):
)
nvr = await self._api.get_nvr()
if await nvr.get_is_prerelease():
if nvr.release_channel != FirmwareReleaseChannel.RELEASE:
return await self.async_step_confirm()
await self.hass.config_entries.async_reload(self._entry.entry_id)
return self.async_create_entry(data={})
@ -124,7 +125,7 @@ async def async_create_fix_flow(
data: dict[str, str | int | float | None] | None,
) -> RepairsFlow:
"""Create flow."""
if data is not None and issue_id == "ea_warning":
if data is not None and issue_id == "ea_channel_warning":
entry_id = cast(str, data["entry_id"])
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
api = async_create_api_client(hass, entry)

View File

@ -45,6 +45,7 @@ INFRARED_MODES = [
{"id": IRLEDMode.AUTO.value, "name": "Auto"},
{"id": IRLEDMode.ON.value, "name": "Always Enable"},
{"id": IRLEDMode.AUTO_NO_LED.value, "name": "Auto (Filter Only, no LED's)"},
{"id": IRLEDMode.CUSTOM.value, "name": "Auto (Custom Lux)"},
{"id": IRLEDMode.OFF.value, "name": "Always Disable"},
]

View File

@ -61,16 +61,16 @@
}
},
"issues": {
"ea_warning": {
"title": "UniFi Protect v{version} is an Early Access version",
"ea_channel_warning": {
"title": "UniFi Protect Early Access enabled",
"fix_flow": {
"step": {
"start": {
"title": "v{version} is an Early Access version",
"description": "You are using v{version} of UniFi Protect which is an Early Access version. [Early Access versions are not supported by Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) and it is recommended to go back to a stable release as soon as possible.\n\nBy submitting this form you have either [downgraded UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) or you agree to run an unsupported version of UniFi Protect."
"title": "UniFi Protect Early Access enabled",
"description": "You are either running an Early Access version of UniFi Protect (v{version}) or opt-ed into a release channel that is not the Official Release Channel. [Home Assistant does not support Early Access versions](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access), so you should immediately switch to the Official Release Channel. Accidentally upgrading to an Early Access version can break your UniFi Protect integration.\n\nBy submitting this form, you have switched back to the Official Release Channel or agree to run an unsupported version of UniFi Protect, which may break your Home Assistant integration at any time."
},
"confirm": {
"title": "[%key:component::unifiprotect::issues::ea_warning::fix_flow::step::start::title%]",
"title": "[%key:component::unifiprotect::issues::ea_channel_warning::fix_flow::step::start::title%]",
"description": "Are you sure you want to run unsupported versions of UniFi Protect? This may cause your Home Assistant integration to break."
}
}
@ -78,7 +78,7 @@
},
"ea_setup_failed": {
"title": "Setup error using Early Access version",
"description": "You are using v{version} of UniFi Protect which is an Early Access version. An unrecoverable error occurred while trying to load the integration. Please [downgrade to a stable version](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) of UniFi Protect to continue using the integration.\n\nError: {error}"
"description": "You are using v{version} of UniFi Protect which is an Early Access version. An unrecoverable error occurred while trying to load the integration. Please restore a backup of a stable release of UniFi Protect to continue using the integration.\n\nError: {error}"
},
"cloud_user": {
"title": "Ubiquiti Cloud Users are not Supported",

View File

@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["croniter"],
"quality_scale": "internal",
"requirements": ["croniter==1.0.6"]
"requirements": ["croniter==2.0.2"]
}

View File

@ -16,7 +16,7 @@ from .helpers.deprecation import (
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "1"
PATCH_VERSION: Final = "2"
__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)

View File

@ -1,6 +1,6 @@
# Automatically generated by gen_requirements_all.py, do not edit
aiodhcpwatcher==0.8.1
aiodhcpwatcher==0.8.2
aiodiscover==1.6.1
aiohttp-fast-url-dispatcher==0.3.0
aiohttp-zlib-ng==0.3.1

View File

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

View File

@ -54,7 +54,7 @@ ProgettiHWSW==0.1.3
# PyBluez==0.22
# homeassistant.components.cast
PyChromecast==14.0.0
PyChromecast==14.0.1
# homeassistant.components.flick_electric
PyFlick==0.0.2
@ -206,7 +206,7 @@ aioaseko==0.0.2
aioasuswrt==1.4.0
# homeassistant.components.husqvarna_automower
aioautomower==2024.3.0
aioautomower==2024.3.3
# homeassistant.components.azure_devops
aioazuredevops==1.3.5
@ -221,7 +221,7 @@ aiobotocore==2.9.1
aiocomelit==0.9.0
# homeassistant.components.dhcp
aiodhcpwatcher==0.8.1
aiodhcpwatcher==0.8.2
# homeassistant.components.dhcp
aiodiscover==1.6.1
@ -350,7 +350,7 @@ aiopyarr==23.4.0
aioqsw==0.3.5
# homeassistant.components.rainforest_raven
aioraven==0.5.1
aioraven==0.5.2
# homeassistant.components.recollect_waste
aiorecollect==2023.09.0
@ -514,7 +514,7 @@ aurorapy==0.2.7
# avion==0.10
# homeassistant.components.axis
axis==54
axis==57
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1
@ -669,7 +669,7 @@ connect-box==0.2.8
construct==2.10.68
# homeassistant.components.utility_meter
croniter==1.0.6
croniter==2.0.2
# homeassistant.components.crownstone
crownstone-cloud==1.4.9
@ -1115,7 +1115,7 @@ ibmiotf==0.3.4
# homeassistant.components.google
# homeassistant.components.local_calendar
# homeassistant.components.local_todo
ical==7.0.1
ical==7.0.3
# homeassistant.components.ping
icmplib==3.0
@ -1971,7 +1971,7 @@ pymitv==1.4.3
pymochad==0.2.0
# homeassistant.components.modbus
pymodbus==3.6.5
pymodbus==3.6.6
# homeassistant.components.monoprice
pymonoprice==0.4
@ -2090,7 +2090,7 @@ pyrecswitch==1.0.2
pyrepetierng==0.1.0
# homeassistant.components.risco
pyrisco==0.5.8
pyrisco==0.5.10
# homeassistant.components.rituals_perfume_genie
pyrituals==0.0.6
@ -2155,7 +2155,7 @@ pysmartthings==0.7.8
pysml==0.0.12
# homeassistant.components.snmp
pysnmp-lextudio==6.0.9
pysnmp-lextudio==6.0.11
# homeassistant.components.snooz
pysnooz==0.8.6
@ -2182,7 +2182,7 @@ pyswitchbee==1.8.0
pytautulli==23.1.1
# homeassistant.components.tedee
pytedee-async==0.2.16
pytedee-async==0.2.17
# homeassistant.components.tfiac
pytfiac==0.4
@ -2340,7 +2340,7 @@ pytrydan==0.4.0
pyudev==0.23.2
# homeassistant.components.unifiprotect
pyunifiprotect==4.23.3
pyunifiprotect==5.0.2
# homeassistant.components.uptimerobot
pyuptimerobot==22.2.0

View File

@ -45,7 +45,7 @@ PlexAPI==4.15.10
ProgettiHWSW==0.1.3
# homeassistant.components.cast
PyChromecast==14.0.0
PyChromecast==14.0.1
# homeassistant.components.flick_electric
PyFlick==0.0.2
@ -185,7 +185,7 @@ aioaseko==0.0.2
aioasuswrt==1.4.0
# homeassistant.components.husqvarna_automower
aioautomower==2024.3.0
aioautomower==2024.3.3
# homeassistant.components.azure_devops
aioazuredevops==1.3.5
@ -200,7 +200,7 @@ aiobotocore==2.9.1
aiocomelit==0.9.0
# homeassistant.components.dhcp
aiodhcpwatcher==0.8.1
aiodhcpwatcher==0.8.2
# homeassistant.components.dhcp
aiodiscover==1.6.1
@ -323,7 +323,7 @@ aiopyarr==23.4.0
aioqsw==0.3.5
# homeassistant.components.rainforest_raven
aioraven==0.5.1
aioraven==0.5.2
# homeassistant.components.recollect_waste
aiorecollect==2023.09.0
@ -454,7 +454,7 @@ auroranoaa==0.0.3
aurorapy==0.2.7
# homeassistant.components.axis
axis==54
axis==57
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1
@ -553,7 +553,7 @@ colorthief==0.2.1
construct==2.10.68
# homeassistant.components.utility_meter
croniter==1.0.6
croniter==2.0.2
# homeassistant.components.crownstone
crownstone-cloud==1.4.9
@ -905,7 +905,7 @@ ibeacon-ble==1.2.0
# homeassistant.components.google
# homeassistant.components.local_calendar
# homeassistant.components.local_todo
ical==7.0.1
ical==7.0.3
# homeassistant.components.ping
icmplib==3.0
@ -1525,7 +1525,7 @@ pymeteoclimatic==0.1.0
pymochad==0.2.0
# homeassistant.components.modbus
pymodbus==3.6.5
pymodbus==3.6.6
# homeassistant.components.monoprice
pymonoprice==0.4
@ -1617,7 +1617,7 @@ pyqwikswitch==0.93
pyrainbird==4.0.2
# homeassistant.components.risco
pyrisco==0.5.8
pyrisco==0.5.10
# homeassistant.components.rituals_perfume_genie
pyrituals==0.0.6
@ -1673,7 +1673,7 @@ pysmartthings==0.7.8
pysml==0.0.12
# homeassistant.components.snmp
pysnmp-lextudio==6.0.9
pysnmp-lextudio==6.0.11
# homeassistant.components.snooz
pysnooz==0.8.6
@ -1697,7 +1697,7 @@ pyswitchbee==1.8.0
pytautulli==23.1.1
# homeassistant.components.tedee
pytedee-async==0.2.16
pytedee-async==0.2.17
# homeassistant.components.motionmount
python-MotionMount==0.3.1
@ -1801,7 +1801,7 @@ pytrydan==0.4.0
pyudev==0.23.2
# homeassistant.components.unifiprotect
pyunifiprotect==4.23.3
pyunifiprotect==5.0.2
# homeassistant.components.uptimerobot
pyuptimerobot==22.2.0

View File

@ -93,10 +93,11 @@ async def test_invalid_hostname(hass: HomeAssistant) -> None:
assert result["errors"] == {CONF_HOST: "wrong_host"}
async def test_connection_error(hass: HomeAssistant) -> None:
@pytest.mark.parametrize("exc", [ConnectionError, TimeoutError])
async def test_connection_error(hass: HomeAssistant, exc: Exception) -> None:
"""Test connection to host error."""
with patch("brother.Brother.initialize"), patch(
"brother.Brother._get_data", side_effect=ConnectionError()
"brother.Brother._get_data", side_effect=exc
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
@ -147,10 +148,11 @@ async def test_device_exists_abort(hass: HomeAssistant) -> None:
assert result["reason"] == "already_configured"
async def test_zeroconf_snmp_error(hass: HomeAssistant) -> None:
"""Test we abort zeroconf flow on SNMP error."""
@pytest.mark.parametrize("exc", [ConnectionError, TimeoutError, SnmpError("error")])
async def test_zeroconf_exception(hass: HomeAssistant, exc: Exception) -> None:
"""Test we abort zeroconf flow on exception."""
with patch("brother.Brother.initialize"), patch(
"brother.Brother._get_data", side_effect=SnmpError("error")
"brother.Brother._get_data", side_effect=exc
):
result = await hass.config_entries.flow.async_init(
DOMAIN,

View File

@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch
from aioautomower.model import MowerModes
from aioautomower.utils import mower_list_to_dictionary_dataclass
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.husqvarna_automower.const import DOMAIN
@ -59,17 +60,36 @@ async def test_cutting_blade_usage_time_sensor(
assert state is not None
assert state.state == "0.034"
entry = hass.config_entries.async_entries(DOMAIN)[0]
await hass.config_entries.async_remove(entry.entry_id)
await hass.async_block_till_done()
@pytest.mark.parametrize(
("sensor_to_test"),
[
("cutting_blade_usage_time"),
("number_of_charging_cycles"),
("number_of_collisions"),
("total_charging_time"),
("total_cutting_time"),
("total_running_time"),
("total_searching_time"),
("total_drive_distance"),
],
)
async def test_statistics_not_available(
hass: HomeAssistant,
mock_automower_client: AsyncMock,
mock_config_entry: MockConfigEntry,
sensor_to_test: str,
) -> None:
"""Test if this sensor is only added, if data is available."""
values = mower_list_to_dictionary_dataclass(
load_json_value_fixture("mower.json", DOMAIN)
)
delattr(values[TEST_MOWER_ID].statistics, "cutting_blade_usage_time")
delattr(values[TEST_MOWER_ID].statistics, sensor_to_test)
mock_automower_client.get_status.return_value = values
await setup_integration(hass, mock_config_entry)
state = hass.states.get("sensor.test_mower_1_cutting_blade_usage_time")
state = hass.states.get(f"sensor.test_mower_1_{sensor_to_test}")
assert state is None

View File

@ -22,6 +22,16 @@
list([
])
# ---
# name: test_parse_existing_ics[invalid_dtstart_tzname]
list([
dict({
'due': '2023-10-24T11:30:00',
'status': 'needs_action',
'summary': 'Task',
'uid': '077cb7f2-6c89-11ee-b2a9-0242ac110002',
}),
])
# ---
# name: test_parse_existing_ics[migrate_legacy_due]
list([
dict({

View File

@ -671,6 +671,28 @@ async def test_move_item_previous_unknown(
),
"1",
),
(
textwrap.dedent(
"""\
BEGIN:VCALENDAR
PRODID:-//homeassistant.io//local_todo 2.0//EN
VERSION:2.0
BEGIN:VTODO
DTSTAMP:20231024T014011
UID:077cb7f2-6c89-11ee-b2a9-0242ac110002
CREATED:20231017T010348
LAST-MODIFIED:20231024T014011
SEQUENCE:1
STATUS:NEEDS-ACTION
SUMMARY:Task
DUE:20231024T113000
DTSTART;TZID=CST:20231024T113000
END:VTODO
END:VCALENDAR
"""
),
"1",
),
],
ids=(
"empty",
@ -679,6 +701,7 @@ async def test_move_item_previous_unknown(
"needs_action",
"migrate_legacy_due",
"due",
"invalid_dtstart_tzname",
),
)
async def test_parse_existing_ics(

View File

@ -155,7 +155,9 @@ MOCK_OWPROXY_DEVICES = {
{ATTR_INJECT_READS: b" 1"},
{ATTR_INJECT_READS: b" 0"},
{ATTR_INJECT_READS: b" 0"},
{ATTR_INJECT_READS: b" 0"},
{
ATTR_INJECT_READS: ProtocolError,
},
{ATTR_INJECT_READS: b" 0"},
{ATTR_INJECT_READS: b" 0"},
{ATTR_INJECT_READS: b" 0"},
@ -165,7 +167,9 @@ MOCK_OWPROXY_DEVICES = {
{ATTR_INJECT_READS: b" 1"},
{ATTR_INJECT_READS: b" 0"},
{ATTR_INJECT_READS: b" 1"},
{ATTR_INJECT_READS: b" 0"},
{
ATTR_INJECT_READS: ProtocolError,
},
{ATTR_INJECT_READS: b" 1"},
{ATTR_INJECT_READS: b" 0"},
{ATTR_INJECT_READS: b" 1"},

View File

@ -851,13 +851,13 @@
'attributes': ReadOnlyDict({
'device_file': '/29.111111111111/sensed.3',
'friendly_name': '29.111111111111 Sensed 3',
'raw_value': 0.0,
'raw_value': None,
}),
'context': <ANY>,
'entity_id': 'binary_sensor.29_111111111111_sensed_3',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'off',
'state': 'unknown',
}),
StateSnapshot({
'attributes': ReadOnlyDict({

View File

@ -1271,13 +1271,13 @@
'attributes': ReadOnlyDict({
'device_file': '/29.111111111111/PIO.3',
'friendly_name': '29.111111111111 Programmed input-output 3',
'raw_value': 0.0,
'raw_value': None,
}),
'context': <ANY>,
'entity_id': 'switch.29_111111111111_programmed_input_output_3',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'off',
'state': 'unknown',
}),
StateSnapshot({
'attributes': ReadOnlyDict({

View File

@ -171,16 +171,6 @@ def connect_with_error(exception):
yield
@pytest.fixture
def connect_with_single_error(exception):
"""Fixture to simulate error on connect."""
with patch(
"homeassistant.components.risco.RiscoLocal.connect",
side_effect=[exception, None],
):
yield
@pytest.fixture
async def setup_risco_local(hass, local_config_entry):
"""Set up a local Risco integration for testing."""

View File

@ -1,21 +0,0 @@
"""Tests for the Risco initialization."""
import pytest
from homeassistant.components.risco import CannotConnectError
from homeassistant.components.risco.const import CONF_COMMUNICATION_DELAY
from homeassistant.core import HomeAssistant
@pytest.mark.parametrize("exception", [CannotConnectError])
async def test_single_error_on_connect(
hass: HomeAssistant, connect_with_single_error, local_config_entry
) -> None:
"""Test single error on connect to validate communication delay update from 0 (default) to 1."""
expected_data = {
**local_config_entry.data,
**{"type": "local", CONF_COMMUNICATION_DELAY: 1},
}
await hass.config_entries.async_setup(local_config_entry.entry_id)
await hass.async_block_till_done()
assert local_config_entry.data == expected_data

View File

@ -67,6 +67,18 @@ async def test_block_reload_on_cfg_change(
mock_block_device.mock_update()
await hass.async_block_till_done()
# Make sure cfgChanged with None is ignored
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "cfgChanged", None)
mock_block_device.mock_update()
await hass.async_block_till_done()
# Wait for debouncer
freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get("switch.test_name_channel_1") is not None
# Generate config change from switch to light
monkeypatch.setitem(
mock_block_device.settings["relays"][RELAY_BLOCK_ID], "appliance_type", "light"

View File

@ -29,6 +29,7 @@ import homeassistant.util.dt as dt_util
from tests.common import (
MockConfigEntry,
assert_setup_component,
async_capture_events,
async_fire_time_changed,
mock_restore_cache_with_extra_data,
)
@ -1848,6 +1849,7 @@ async def test_trigger_entity_restore_state(
"my_variable": "{{ trigger.event.data.beer + 1 }}"
},
},
{"event": "test_event2", "event_data": {"hello": "world"}},
],
"sensor": [
{
@ -1864,6 +1866,10 @@ async def test_trigger_action(
hass: HomeAssistant, start_ha, entity_registry: er.EntityRegistry
) -> None:
"""Test trigger entity with an action works."""
event = "test_event2"
context = Context()
events = async_capture_events(hass, event)
state = hass.states.get("sensor.hello_name")
assert state is not None
assert state.state == STATE_UNKNOWN
@ -1875,3 +1881,6 @@ async def test_trigger_action(
state = hass.states.get("sensor.hello_name")
assert state.state == "3"
assert state.context is context
assert len(events) == 1
assert events[0].context.parent_id == context.id

View File

@ -34,7 +34,7 @@
'uniqueId': 'abc123',
}),
'geofence': dict({
'area': 'string',
'area': '**REDACTED**',
'attributes': dict({
}),
'calendarId': 0,
@ -134,7 +134,7 @@
'uniqueId': 'abc123',
}),
'geofence': dict({
'area': 'string',
'area': '**REDACTED**',
'attributes': dict({
}),
'calendarId': 0,

View File

@ -318,7 +318,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) -
"disable_rtsp": True,
"override_connection_host": True,
"max_media": 1000,
"allow_ea": False,
"allow_ea_channel": False,
}
await hass.config_entries.async_unload(mock_config.entry_id)

View File

@ -45,12 +45,14 @@ async def test_ea_warning_ignore(
assert len(msg["result"]["issues"]) > 0
issue = None
for i in msg["result"]["issues"]:
if i["issue_id"] == "ea_warning":
if i["issue_id"] == "ea_channel_warning":
issue = i
assert issue is not None
url = RepairsFlowIndexView.url
resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "ea_warning"})
resp = await client.post(
url, json={"handler": DOMAIN, "issue_id": "ea_channel_warning"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
@ -103,12 +105,14 @@ async def test_ea_warning_fix(
assert len(msg["result"]["issues"]) > 0
issue = None
for i in msg["result"]["issues"]:
if i["issue_id"] == "ea_warning":
if i["issue_id"] == "ea_channel_warning":
issue = i
assert issue is not None
url = RepairsFlowIndexView.url
resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "ea_warning"})
resp = await client.post(
url, json={"handler": DOMAIN, "issue_id": "ea_channel_warning"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
@ -121,8 +125,9 @@ async def test_ea_warning_fix(
new_nvr = copy(ufp.api.bootstrap.nvr)
new_nvr.version = Version("2.2.6")
new_nvr.release_channel = "release"
mock_msg = Mock()
mock_msg.changed_data = {"version": "2.2.6"}
mock_msg.changed_data = {"version": "2.2.6", "releaseChannel": "release"}
mock_msg.new_obj = new_nvr
ufp.api.bootstrap.nvr = new_nvr