mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
2024.3.2 (#113973)
* 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:
parent
f7972ce9b2
commit
f10d924e8b
@ -105,6 +105,7 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
|
|||||||
),
|
),
|
||||||
"illuminance": SensorEntityDescription(
|
"illuminance": SensorEntityDescription(
|
||||||
key="illuminance",
|
key="illuminance",
|
||||||
|
translation_key="illuminance",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
|
@ -33,6 +33,9 @@
|
|||||||
},
|
},
|
||||||
"radon_longterm_level": {
|
"radon_longterm_level": {
|
||||||
"name": "Radon longterm level"
|
"name": "Radon longterm level"
|
||||||
|
},
|
||||||
|
"illuminance": {
|
||||||
|
"name": "[%key:component::sensor::entity_component::illuminance::name%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["axis"],
|
"loggers": ["axis"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["axis==54"],
|
"requirements": ["axis==57"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "AXIS"
|
"manufacturer": "AXIS"
|
||||||
|
@ -58,7 +58,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
return self.async_create_entry(title=title, data=user_input)
|
return self.async_create_entry(title=title, data=user_input)
|
||||||
except InvalidHost:
|
except InvalidHost:
|
||||||
errors[CONF_HOST] = "wrong_host"
|
errors[CONF_HOST] = "wrong_host"
|
||||||
except ConnectionError:
|
except (ConnectionError, TimeoutError):
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except SnmpError:
|
except SnmpError:
|
||||||
errors["base"] = "snmp_error"
|
errors["base"] = "snmp_error"
|
||||||
@ -88,7 +88,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
await self.brother.async_update()
|
await self.brother.async_update()
|
||||||
except UnsupportedModelError:
|
except UnsupportedModelError:
|
||||||
return self.async_abort(reason="unsupported_model")
|
return self.async_abort(reason="unsupported_model")
|
||||||
except (ConnectionError, SnmpError):
|
except (ConnectionError, SnmpError, TimeoutError):
|
||||||
return self.async_abort(reason="cannot_connect")
|
return self.async_abort(reason="cannot_connect")
|
||||||
|
|
||||||
# Check if already configured
|
# Check if already configured
|
||||||
|
@ -24,9 +24,9 @@ PLATFORMS = [Platform.MEDIA_PLAYER]
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up Cast from a config entry."""
|
"""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 home_assistant_cast.async_setup_ha_cast(hass, entry)
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
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)
|
await async_process_integration_platforms(hass, DOMAIN, _register_cast_platform)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -14,6 +14,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/cast",
|
"documentation": "https://www.home-assistant.io/integrations/cast",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["casttube", "pychromecast"],
|
"loggers": ["casttube", "pychromecast"],
|
||||||
"requirements": ["PyChromecast==14.0.0"],
|
"requirements": ["PyChromecast==14.0.1"],
|
||||||
"zeroconf": ["_googlecast._tcp.local."]
|
"zeroconf": ["_googlecast._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,10 @@ from __future__ import annotations
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from functools import wraps
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar
|
||||||
|
|
||||||
import pychromecast
|
import pychromecast
|
||||||
from pychromecast.controllers.homeassistant import HomeAssistantController
|
from pychromecast.controllers.homeassistant import HomeAssistantController
|
||||||
@ -18,6 +19,7 @@ from pychromecast.controllers.media import (
|
|||||||
)
|
)
|
||||||
from pychromecast.controllers.multizone import MultizoneManager
|
from pychromecast.controllers.multizone import MultizoneManager
|
||||||
from pychromecast.controllers.receiver import VOLUME_CONTROL_TYPE_FIXED
|
from pychromecast.controllers.receiver import VOLUME_CONTROL_TYPE_FIXED
|
||||||
|
from pychromecast.error import PyChromecastError
|
||||||
from pychromecast.quick_play import quick_play
|
from pychromecast.quick_play import quick_play
|
||||||
from pychromecast.socket_client import (
|
from pychromecast.socket_client import (
|
||||||
CONNECTION_STATUS_CONNECTED,
|
CONNECTION_STATUS_CONNECTED,
|
||||||
@ -83,6 +85,34 @@ APP_IDS_UNRELIABLE_MEDIA_INFO = ("Netflix",)
|
|||||||
CAST_SPLASH = "https://www.home-assistant.io/images/cast/splash.png"
|
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
|
@callback
|
||||||
def _async_create_cast_device(hass: HomeAssistant, info: ChromecastInfo):
|
def _async_create_cast_device(hass: HomeAssistant, info: ChromecastInfo):
|
||||||
"""Create a CastDevice entity or dynamic group from the chromecast object.
|
"""Create a CastDevice entity or dynamic group from the chromecast object.
|
||||||
@ -476,6 +506,21 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
|||||||
|
|
||||||
return media_controller
|
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:
|
def turn_on(self) -> None:
|
||||||
"""Turn on the cast device."""
|
"""Turn on the cast device."""
|
||||||
|
|
||||||
@ -486,52 +531,61 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
|||||||
|
|
||||||
if chromecast.app_id is not None:
|
if chromecast.app_id is not None:
|
||||||
# Quit the previous app before starting splash screen or media player
|
# 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
|
# The only way we can turn the Chromecast is on is by launching an app
|
||||||
if chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST:
|
if chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST:
|
||||||
app_data = {"media_id": CAST_SPLASH, "media_type": "image/png"}
|
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:
|
else:
|
||||||
chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER)
|
self._start_app(pychromecast.config.APP_MEDIA_RECEIVER)
|
||||||
|
|
||||||
|
@api_error
|
||||||
def turn_off(self) -> None:
|
def turn_off(self) -> None:
|
||||||
"""Turn off the cast device."""
|
"""Turn off the cast device."""
|
||||||
self._get_chromecast().quit_app()
|
self._get_chromecast().quit_app()
|
||||||
|
|
||||||
|
@api_error
|
||||||
def mute_volume(self, mute: bool) -> None:
|
def mute_volume(self, mute: bool) -> None:
|
||||||
"""Mute the volume."""
|
"""Mute the volume."""
|
||||||
self._get_chromecast().set_volume_muted(mute)
|
self._get_chromecast().set_volume_muted(mute)
|
||||||
|
|
||||||
|
@api_error
|
||||||
def set_volume_level(self, volume: float) -> None:
|
def set_volume_level(self, volume: float) -> None:
|
||||||
"""Set volume level, range 0..1."""
|
"""Set volume level, range 0..1."""
|
||||||
self._get_chromecast().set_volume(volume)
|
self._get_chromecast().set_volume(volume)
|
||||||
|
|
||||||
|
@api_error
|
||||||
def media_play(self) -> None:
|
def media_play(self) -> None:
|
||||||
"""Send play command."""
|
"""Send play command."""
|
||||||
media_controller = self._media_controller()
|
media_controller = self._media_controller()
|
||||||
media_controller.play()
|
media_controller.play()
|
||||||
|
|
||||||
|
@api_error
|
||||||
def media_pause(self) -> None:
|
def media_pause(self) -> None:
|
||||||
"""Send pause command."""
|
"""Send pause command."""
|
||||||
media_controller = self._media_controller()
|
media_controller = self._media_controller()
|
||||||
media_controller.pause()
|
media_controller.pause()
|
||||||
|
|
||||||
|
@api_error
|
||||||
def media_stop(self) -> None:
|
def media_stop(self) -> None:
|
||||||
"""Send stop command."""
|
"""Send stop command."""
|
||||||
media_controller = self._media_controller()
|
media_controller = self._media_controller()
|
||||||
media_controller.stop()
|
media_controller.stop()
|
||||||
|
|
||||||
|
@api_error
|
||||||
def media_previous_track(self) -> None:
|
def media_previous_track(self) -> None:
|
||||||
"""Send previous track command."""
|
"""Send previous track command."""
|
||||||
media_controller = self._media_controller()
|
media_controller = self._media_controller()
|
||||||
media_controller.queue_prev()
|
media_controller.queue_prev()
|
||||||
|
|
||||||
|
@api_error
|
||||||
def media_next_track(self) -> None:
|
def media_next_track(self) -> None:
|
||||||
"""Send next track command."""
|
"""Send next track command."""
|
||||||
media_controller = self._media_controller()
|
media_controller = self._media_controller()
|
||||||
media_controller.queue_next()
|
media_controller.queue_next()
|
||||||
|
|
||||||
|
@api_error
|
||||||
def media_seek(self, position: float) -> None:
|
def media_seek(self, position: float) -> None:
|
||||||
"""Seek the media to a specific location."""
|
"""Seek the media to a specific location."""
|
||||||
media_controller = self._media_controller()
|
media_controller = self._media_controller()
|
||||||
@ -644,7 +698,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
|||||||
if "app_id" in app_data:
|
if "app_id" in app_data:
|
||||||
app_id = app_data.pop("app_id")
|
app_id = app_data.pop("app_id")
|
||||||
_LOGGER.info("Starting Cast app by ID %s", 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:
|
if app_data:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Extra keys %s were ignored. Please use app_name to cast media",
|
"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")
|
app_name = app_data.pop("app_name")
|
||||||
try:
|
try:
|
||||||
await self.hass.async_add_executor_job(
|
await self.hass.async_add_executor_job(
|
||||||
quick_play, chromecast, app_name, app_data
|
self._quick_play, app_name, app_data
|
||||||
)
|
)
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
_LOGGER.error("App %s not supported", app_name)
|
_LOGGER.error("App %s not supported", app_name)
|
||||||
@ -729,7 +783,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
|||||||
app_data,
|
app_data,
|
||||||
)
|
)
|
||||||
await self.hass.async_add_executor_job(
|
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):
|
def _media_status(self):
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
],
|
],
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"aiodhcpwatcher==0.8.1",
|
"aiodhcpwatcher==0.8.2",
|
||||||
"aiodiscover==1.6.1",
|
"aiodiscover==1.6.1",
|
||||||
"cached_ipaddress==0.3.0"
|
"cached_ipaddress==0.3.0"
|
||||||
]
|
]
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/calendar.google",
|
"documentation": "https://www.home-assistant.io/integrations/calendar.google",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["googleapiclient"],
|
"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"]
|
||||||
}
|
}
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower",
|
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioautomower"],
|
"loggers": ["aioautomower"],
|
||||||
"requirements": ["aioautomower==2024.3.0"]
|
"requirements": ["aioautomower==2024.3.3"]
|
||||||
}
|
}
|
||||||
|
@ -69,6 +69,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
|
|||||||
device_class=SensorDeviceClass.DURATION,
|
device_class=SensorDeviceClass.DURATION,
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
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,
|
value_fn=lambda data: data.statistics.total_charging_time,
|
||||||
),
|
),
|
||||||
AutomowerSensorEntityDescription(
|
AutomowerSensorEntityDescription(
|
||||||
@ -79,6 +80,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
|
|||||||
device_class=SensorDeviceClass.DURATION,
|
device_class=SensorDeviceClass.DURATION,
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
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,
|
value_fn=lambda data: data.statistics.total_cutting_time,
|
||||||
),
|
),
|
||||||
AutomowerSensorEntityDescription(
|
AutomowerSensorEntityDescription(
|
||||||
@ -89,6 +91,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
|
|||||||
device_class=SensorDeviceClass.DURATION,
|
device_class=SensorDeviceClass.DURATION,
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
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,
|
value_fn=lambda data: data.statistics.total_running_time,
|
||||||
),
|
),
|
||||||
AutomowerSensorEntityDescription(
|
AutomowerSensorEntityDescription(
|
||||||
@ -99,6 +102,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
|
|||||||
device_class=SensorDeviceClass.DURATION,
|
device_class=SensorDeviceClass.DURATION,
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
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,
|
value_fn=lambda data: data.statistics.total_searching_time,
|
||||||
),
|
),
|
||||||
AutomowerSensorEntityDescription(
|
AutomowerSensorEntityDescription(
|
||||||
@ -107,6 +111,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
|
|||||||
icon="mdi:battery-sync-outline",
|
icon="mdi:battery-sync-outline",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
state_class=SensorStateClass.TOTAL,
|
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,
|
value_fn=lambda data: data.statistics.number_of_charging_cycles,
|
||||||
),
|
),
|
||||||
AutomowerSensorEntityDescription(
|
AutomowerSensorEntityDescription(
|
||||||
@ -115,6 +120,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
|
|||||||
icon="mdi:counter",
|
icon="mdi:counter",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
state_class=SensorStateClass.TOTAL,
|
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,
|
value_fn=lambda data: data.statistics.number_of_collisions,
|
||||||
),
|
),
|
||||||
AutomowerSensorEntityDescription(
|
AutomowerSensorEntityDescription(
|
||||||
@ -125,6 +131,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
|
|||||||
device_class=SensorDeviceClass.DISTANCE,
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
native_unit_of_measurement=UnitOfLength.METERS,
|
native_unit_of_measurement=UnitOfLength.METERS,
|
||||||
suggested_unit_of_measurement=UnitOfLength.KILOMETERS,
|
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,
|
value_fn=lambda data: data.statistics.total_drive_distance,
|
||||||
),
|
),
|
||||||
AutomowerSensorEntityDescription(
|
AutomowerSensorEntityDescription(
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["ical"],
|
"loggers": ["ical"],
|
||||||
"requirements": ["ical==7.0.1"]
|
"requirements": ["ical==7.0.3"]
|
||||||
}
|
}
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["ical==7.0.1"]
|
"requirements": ["ical==7.0.3"]
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pymodbus"],
|
"loggers": ["pymodbus"],
|
||||||
"quality_scale": "gold",
|
"quality_scale": "gold",
|
||||||
"requirements": ["pymodbus==3.6.5"]
|
"requirements": ["pymodbus==3.6.6"]
|
||||||
}
|
}
|
||||||
|
@ -165,9 +165,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
except NotionError as err:
|
except NotionError as err:
|
||||||
raise ConfigEntryNotReady("Config entry failed to load") from err
|
raise ConfigEntryNotReady("Config entry failed to load") from err
|
||||||
|
|
||||||
# Always update the config entry with the latest refresh token and user UUID:
|
# Update the Notion user UUID and refresh token if they've changed:
|
||||||
entry_updates["data"][CONF_REFRESH_TOKEN] = client.refresh_token
|
for key, value in (
|
||||||
entry_updates["data"][CONF_USER_UUID] = client.user_uuid
|
(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
|
@callback
|
||||||
def async_save_refresh_token(refresh_token: str) -> None:
|
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:
|
# Create a callback to save the refresh token when it changes:
|
||||||
entry.async_on_unload(client.add_refresh_token_callback(async_save_refresh_token))
|
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:
|
async def async_update() -> NotionData:
|
||||||
"""Get the latest data from the Notion API."""
|
"""Get the latest data from the Notion API."""
|
||||||
data = NotionData(hass=hass, entry=entry)
|
data = NotionData(hass=hass, entry=entry)
|
||||||
|
@ -143,6 +143,8 @@ class OneWireBinarySensor(OneWireEntity, BinarySensorEntity):
|
|||||||
entity_description: OneWireBinarySensorEntityDescription
|
entity_description: OneWireBinarySensorEntityDescription
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool | None:
|
||||||
"""Return true if sensor is on."""
|
"""Return true if sensor is on."""
|
||||||
|
if self._state is None:
|
||||||
|
return None
|
||||||
return bool(self._state)
|
return bool(self._state)
|
||||||
|
@ -204,8 +204,10 @@ class OneWireSwitch(OneWireEntity, SwitchEntity):
|
|||||||
entity_description: OneWireSwitchEntityDescription
|
entity_description: OneWireSwitchEntityDescription
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool | None:
|
||||||
"""Return true if sensor is on."""
|
"""Return true if switch is on."""
|
||||||
|
if self._state is None:
|
||||||
|
return None
|
||||||
return bool(self._state)
|
return bool(self._state)
|
||||||
|
|
||||||
def turn_on(self, **kwargs: Any) -> None:
|
def turn_on(self, **kwargs: Any) -> None:
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"dependencies": ["usb"],
|
"dependencies": ["usb"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/rainforest_raven",
|
"documentation": "https://www.home-assistant.io/integrations/rainforest_raven",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["aioraven==0.5.1"],
|
"requirements": ["aioraven==0.5.2"],
|
||||||
"usb": [
|
"usb": [
|
||||||
{
|
{
|
||||||
"vid": "0403",
|
"vid": "0403",
|
||||||
|
@ -37,12 +37,10 @@ from homeassistant.helpers.storage import Store
|
|||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_COMMUNICATION_DELAY,
|
|
||||||
DATA_COORDINATOR,
|
DATA_COORDINATOR,
|
||||||
DEFAULT_SCAN_INTERVAL,
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVENTS_COORDINATOR,
|
EVENTS_COORDINATOR,
|
||||||
MAX_COMMUNICATION_DELAY,
|
|
||||||
TYPE_LOCAL,
|
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:
|
async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
data = entry.data
|
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:
|
try:
|
||||||
await risco.connect()
|
await risco.connect()
|
||||||
except CannotConnectError as error:
|
except CannotConnectError as error:
|
||||||
if comm_delay >= MAX_COMMUNICATION_DELAY:
|
|
||||||
raise ConfigEntryNotReady() from error
|
raise ConfigEntryNotReady() from error
|
||||||
comm_delay += 1
|
|
||||||
except UnauthorizedError:
|
except UnauthorizedError:
|
||||||
_LOGGER.exception("Failed to login to Risco cloud")
|
_LOGGER.exception("Failed to login to Risco cloud")
|
||||||
return False
|
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)
|
|
||||||
|
|
||||||
async def _error(error: Exception) -> None:
|
async def _error(error: Exception) -> None:
|
||||||
_LOGGER.error("Error in Risco library: %s", error)
|
_LOGGER.error("Error in Risco library: %s", error)
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pyrisco"],
|
"loggers": ["pyrisco"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pyrisco==0.5.8"]
|
"requirements": ["pyrisco==0.5.10"]
|
||||||
}
|
}
|
||||||
|
@ -216,7 +216,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]):
|
|||||||
# Check for input events and config change
|
# Check for input events and config change
|
||||||
cfg_changed = 0
|
cfg_changed = 0
|
||||||
for block in self.device.blocks:
|
for block in self.device.blocks:
|
||||||
if block.type == "device":
|
if block.type == "device" and block.cfgChanged is not None:
|
||||||
cfg_changed = block.cfgChanged
|
cfg_changed = block.cfgChanged
|
||||||
|
|
||||||
# Shelly TRV sends information about changing the configuration for no
|
# Shelly TRV sends information about changing the configuration for no
|
||||||
|
@ -941,6 +941,7 @@ RPC_SENSORS: Final = {
|
|||||||
device_class=SensorDeviceClass.BATTERY,
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
removal_condition=lambda _config, status, key: (status[key]["battery"] is None),
|
||||||
),
|
),
|
||||||
"voltmeter": RpcSensorDescription(
|
"voltmeter": RpcSensorDescription(
|
||||||
key="voltmeter",
|
key="voltmeter",
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/snmp",
|
"documentation": "https://www.home-assistant.io/integrations/snmp",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pyasn1", "pysmi", "pysnmp"],
|
"loggers": ["pyasn1", "pysmi", "pysnmp"],
|
||||||
"requirements": ["pysnmp-lextudio==6.0.9"]
|
"requirements": ["pysnmp-lextudio==6.0.11"]
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import PyTado
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.water_heater import (
|
from homeassistant.components.water_heater import (
|
||||||
@ -29,8 +28,6 @@ from .const import (
|
|||||||
DATA,
|
DATA,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SIGNAL_TADO_UPDATE_RECEIVED,
|
SIGNAL_TADO_UPDATE_RECEIVED,
|
||||||
TADO_DEFAULT_MAX_TEMP,
|
|
||||||
TADO_DEFAULT_MIN_TEMP,
|
|
||||||
TYPE_HOT_WATER,
|
TYPE_HOT_WATER,
|
||||||
)
|
)
|
||||||
from .entity import TadoZoneEntity
|
from .entity import TadoZoneEntity
|
||||||
@ -133,8 +130,8 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
|
|||||||
zone_name: str,
|
zone_name: str,
|
||||||
zone_id: int,
|
zone_id: int,
|
||||||
supports_temperature_control: bool,
|
supports_temperature_control: bool,
|
||||||
min_temp: float | None = None,
|
min_temp,
|
||||||
max_temp: float | None = None,
|
max_temp,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize of Tado water heater entity."""
|
"""Initialize of Tado water heater entity."""
|
||||||
self._tado = tado
|
self._tado = tado
|
||||||
@ -146,8 +143,8 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
|
|||||||
self._device_is_active = False
|
self._device_is_active = False
|
||||||
|
|
||||||
self._supports_temperature_control = supports_temperature_control
|
self._supports_temperature_control = supports_temperature_control
|
||||||
self._min_temperature = min_temp or TADO_DEFAULT_MIN_TEMP
|
self._min_temperature = min_temp
|
||||||
self._max_temperature = max_temp or TADO_DEFAULT_MAX_TEMP
|
self._max_temperature = max_temp
|
||||||
|
|
||||||
self._target_temp: float | None = None
|
self._target_temp: float | None = None
|
||||||
|
|
||||||
@ -157,7 +154,7 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
|
|||||||
|
|
||||||
self._current_tado_hvac_mode = CONST_MODE_SMART_SCHEDULE
|
self._current_tado_hvac_mode = CONST_MODE_SMART_SCHEDULE
|
||||||
self._overlay_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:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Register for sensor updates."""
|
"""Register for sensor updates."""
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/tedee",
|
"documentation": "https://www.home-assistant.io/integrations/tedee",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pytedee_async"],
|
"loggers": ["pytedee_async"],
|
||||||
"requirements": ["pytedee-async==0.2.16"]
|
"requirements": ["pytedee-async==0.2.17"]
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ from collections.abc import Callable
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
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 import discovery, trigger as trigger_helper
|
||||||
from homeassistant.helpers.script import Script
|
from homeassistant.helpers.script import Script
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
@ -90,7 +90,10 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def _handle_triggered_with_script(self, run_variables, context=None):
|
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
|
run_variables = script_result.variables
|
||||||
self._handle_triggered(run_variables, context)
|
self._handle_triggered(run_variables, context)
|
||||||
|
|
||||||
|
@ -12,7 +12,12 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
|
|||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import TraccarServerCoordinator
|
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(
|
async def async_get_config_entry_diagnostics(
|
||||||
|
@ -6,6 +6,7 @@ import logging
|
|||||||
|
|
||||||
from aiohttp.client_exceptions import ServerDisconnectedError
|
from aiohttp.client_exceptions import ServerDisconnectedError
|
||||||
from pyunifiprotect.data import Bootstrap
|
from pyunifiprotect.data import Bootstrap
|
||||||
|
from pyunifiprotect.data.types import FirmwareReleaseChannel
|
||||||
from pyunifiprotect.exceptions import ClientError, NotAuthorized
|
from pyunifiprotect.exceptions import ClientError, NotAuthorized
|
||||||
|
|
||||||
# Import the test_util.anonymize module from the pyunifiprotect package
|
# 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)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, data_service.async_stop)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if not entry.options.get(CONF_ALLOW_EA, False) and (
|
||||||
not entry.options.get(CONF_ALLOW_EA, False)
|
await nvr_info.get_is_prerelease()
|
||||||
and await nvr_info.get_is_prerelease()
|
or nvr_info.release_channel != FirmwareReleaseChannel.RELEASE
|
||||||
):
|
):
|
||||||
ir.async_create_issue(
|
ir.async_create_issue(
|
||||||
hass,
|
hass,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
"ea_warning",
|
"ea_channel_warning",
|
||||||
is_fixable=True,
|
is_fixable=True,
|
||||||
is_persistent=True,
|
is_persistent=True,
|
||||||
learn_more_url="https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access",
|
learn_more_url="https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access",
|
||||||
severity=IssueSeverity.WARNING,
|
severity=IssueSeverity.WARNING,
|
||||||
translation_key="ea_warning",
|
translation_key="ea_channel_warning",
|
||||||
translation_placeholders={"version": str(nvr_info.version)},
|
translation_placeholders={"version": str(nvr_info.version)},
|
||||||
data={"entry_id": entry.entry_id},
|
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),
|
"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)
|
_LOGGER.exception("Error setting up UniFi Protect integration: %s", err)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ CONF_DISABLE_RTSP = "disable_rtsp"
|
|||||||
CONF_ALL_UPDATES = "all_updates"
|
CONF_ALL_UPDATES = "all_updates"
|
||||||
CONF_OVERRIDE_CHOST = "override_connection_host"
|
CONF_OVERRIDE_CHOST = "override_connection_host"
|
||||||
CONF_MAX_MEDIA = "max_media"
|
CONF_MAX_MEDIA = "max_media"
|
||||||
CONF_ALLOW_EA = "allow_ea"
|
CONF_ALLOW_EA = "allow_ea_channel"
|
||||||
|
|
||||||
CONFIG_OPTIONS = [
|
CONFIG_OPTIONS = [
|
||||||
CONF_ALL_UPDATES,
|
CONF_ALL_UPDATES,
|
||||||
|
@ -19,6 +19,7 @@ from pyunifiprotect.data import (
|
|||||||
WSSubscriptionMessage,
|
WSSubscriptionMessage,
|
||||||
)
|
)
|
||||||
from pyunifiprotect.exceptions import ClientError, NotAuthorized
|
from pyunifiprotect.exceptions import ClientError, NotAuthorized
|
||||||
|
from pyunifiprotect.utils import log_event
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
ProtectDeviceType = ProtectAdoptableDeviceModel | NVR
|
ProtectDeviceType = ProtectAdoptableDeviceModel | NVR
|
||||||
SMART_EVENTS = {
|
|
||||||
EventType.SMART_DETECT,
|
|
||||||
EventType.SMART_AUDIO_DETECT,
|
|
||||||
EventType.SMART_DETECT_LINE,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -230,26 +226,7 @@ class ProtectData:
|
|||||||
# trigger updates for camera that the event references
|
# trigger updates for camera that the event references
|
||||||
elif isinstance(obj, Event): # type: ignore[unreachable]
|
elif isinstance(obj, Event): # type: ignore[unreachable]
|
||||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||||
_LOGGER.debug("event WS msg: %s", obj.dict())
|
log_event(obj)
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
if obj.type is EventType.DEVICE_ADOPTED:
|
if obj.type is EventType.DEVICE_ADOPTED:
|
||||||
if obj.metadata is not None and obj.metadata.device_id is not None:
|
if obj.metadata is not None and obj.metadata.device_id is not None:
|
||||||
device = self.api.bootstrap.get_device_from_id(
|
device = self.api.bootstrap.get_device_from_id(
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pyunifiprotect", "unifi_discovery"],
|
"loggers": ["pyunifiprotect", "unifi_discovery"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pyunifiprotect==4.23.3", "unifi-discovery==1.1.8"],
|
"requirements": ["pyunifiprotect==5.0.2", "unifi-discovery==1.1.8"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Ubiquiti Networks",
|
"manufacturer": "Ubiquiti Networks",
|
||||||
|
@ -6,6 +6,7 @@ import logging
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from pyunifiprotect import ProtectApiClient
|
from pyunifiprotect import ProtectApiClient
|
||||||
|
from pyunifiprotect.data.types import FirmwareReleaseChannel
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import data_entry_flow
|
from homeassistant import data_entry_flow
|
||||||
@ -68,7 +69,7 @@ class EAConfirm(ProtectRepair):
|
|||||||
)
|
)
|
||||||
|
|
||||||
nvr = await self._api.get_nvr()
|
nvr = await self._api.get_nvr()
|
||||||
if await nvr.get_is_prerelease():
|
if nvr.release_channel != FirmwareReleaseChannel.RELEASE:
|
||||||
return await self.async_step_confirm()
|
return await self.async_step_confirm()
|
||||||
await self.hass.config_entries.async_reload(self._entry.entry_id)
|
await self.hass.config_entries.async_reload(self._entry.entry_id)
|
||||||
return self.async_create_entry(data={})
|
return self.async_create_entry(data={})
|
||||||
@ -124,7 +125,7 @@ async def async_create_fix_flow(
|
|||||||
data: dict[str, str | int | float | None] | None,
|
data: dict[str, str | int | float | None] | None,
|
||||||
) -> RepairsFlow:
|
) -> RepairsFlow:
|
||||||
"""Create flow."""
|
"""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"])
|
entry_id = cast(str, data["entry_id"])
|
||||||
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
|
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
|
||||||
api = async_create_api_client(hass, entry)
|
api = async_create_api_client(hass, entry)
|
||||||
|
@ -45,6 +45,7 @@ INFRARED_MODES = [
|
|||||||
{"id": IRLEDMode.AUTO.value, "name": "Auto"},
|
{"id": IRLEDMode.AUTO.value, "name": "Auto"},
|
||||||
{"id": IRLEDMode.ON.value, "name": "Always Enable"},
|
{"id": IRLEDMode.ON.value, "name": "Always Enable"},
|
||||||
{"id": IRLEDMode.AUTO_NO_LED.value, "name": "Auto (Filter Only, no LED's)"},
|
{"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"},
|
{"id": IRLEDMode.OFF.value, "name": "Always Disable"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -61,16 +61,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"issues": {
|
"issues": {
|
||||||
"ea_warning": {
|
"ea_channel_warning": {
|
||||||
"title": "UniFi Protect v{version} is an Early Access version",
|
"title": "UniFi Protect Early Access enabled",
|
||||||
"fix_flow": {
|
"fix_flow": {
|
||||||
"step": {
|
"step": {
|
||||||
"start": {
|
"start": {
|
||||||
"title": "v{version} is an Early Access version",
|
"title": "UniFi Protect Early Access enabled",
|
||||||
"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."
|
"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": {
|
"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."
|
"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": {
|
"ea_setup_failed": {
|
||||||
"title": "Setup error using Early Access version",
|
"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": {
|
"cloud_user": {
|
||||||
"title": "Ubiquiti Cloud Users are not Supported",
|
"title": "Ubiquiti Cloud Users are not Supported",
|
||||||
|
@ -8,5 +8,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["croniter"],
|
"loggers": ["croniter"],
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["croniter==1.0.6"]
|
"requirements": ["croniter==2.0.2"]
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ from .helpers.deprecation import (
|
|||||||
APPLICATION_NAME: Final = "HomeAssistant"
|
APPLICATION_NAME: Final = "HomeAssistant"
|
||||||
MAJOR_VERSION: Final = 2024
|
MAJOR_VERSION: Final = 2024
|
||||||
MINOR_VERSION: Final = 3
|
MINOR_VERSION: Final = 3
|
||||||
PATCH_VERSION: Final = "1"
|
PATCH_VERSION: Final = "2"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Automatically generated by gen_requirements_all.py, do not edit
|
# Automatically generated by gen_requirements_all.py, do not edit
|
||||||
|
|
||||||
aiodhcpwatcher==0.8.1
|
aiodhcpwatcher==0.8.2
|
||||||
aiodiscover==1.6.1
|
aiodiscover==1.6.1
|
||||||
aiohttp-fast-url-dispatcher==0.3.0
|
aiohttp-fast-url-dispatcher==0.3.0
|
||||||
aiohttp-zlib-ng==0.3.1
|
aiohttp-zlib-ng==0.3.1
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2024.3.1"
|
version = "2024.3.2"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "Open-source home automation platform running on Python 3."
|
description = "Open-source home automation platform running on Python 3."
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
|
@ -54,7 +54,7 @@ ProgettiHWSW==0.1.3
|
|||||||
# PyBluez==0.22
|
# PyBluez==0.22
|
||||||
|
|
||||||
# homeassistant.components.cast
|
# homeassistant.components.cast
|
||||||
PyChromecast==14.0.0
|
PyChromecast==14.0.1
|
||||||
|
|
||||||
# homeassistant.components.flick_electric
|
# homeassistant.components.flick_electric
|
||||||
PyFlick==0.0.2
|
PyFlick==0.0.2
|
||||||
@ -206,7 +206,7 @@ aioaseko==0.0.2
|
|||||||
aioasuswrt==1.4.0
|
aioasuswrt==1.4.0
|
||||||
|
|
||||||
# homeassistant.components.husqvarna_automower
|
# homeassistant.components.husqvarna_automower
|
||||||
aioautomower==2024.3.0
|
aioautomower==2024.3.3
|
||||||
|
|
||||||
# homeassistant.components.azure_devops
|
# homeassistant.components.azure_devops
|
||||||
aioazuredevops==1.3.5
|
aioazuredevops==1.3.5
|
||||||
@ -221,7 +221,7 @@ aiobotocore==2.9.1
|
|||||||
aiocomelit==0.9.0
|
aiocomelit==0.9.0
|
||||||
|
|
||||||
# homeassistant.components.dhcp
|
# homeassistant.components.dhcp
|
||||||
aiodhcpwatcher==0.8.1
|
aiodhcpwatcher==0.8.2
|
||||||
|
|
||||||
# homeassistant.components.dhcp
|
# homeassistant.components.dhcp
|
||||||
aiodiscover==1.6.1
|
aiodiscover==1.6.1
|
||||||
@ -350,7 +350,7 @@ aiopyarr==23.4.0
|
|||||||
aioqsw==0.3.5
|
aioqsw==0.3.5
|
||||||
|
|
||||||
# homeassistant.components.rainforest_raven
|
# homeassistant.components.rainforest_raven
|
||||||
aioraven==0.5.1
|
aioraven==0.5.2
|
||||||
|
|
||||||
# homeassistant.components.recollect_waste
|
# homeassistant.components.recollect_waste
|
||||||
aiorecollect==2023.09.0
|
aiorecollect==2023.09.0
|
||||||
@ -514,7 +514,7 @@ aurorapy==0.2.7
|
|||||||
# avion==0.10
|
# avion==0.10
|
||||||
|
|
||||||
# homeassistant.components.axis
|
# homeassistant.components.axis
|
||||||
axis==54
|
axis==57
|
||||||
|
|
||||||
# homeassistant.components.azure_event_hub
|
# homeassistant.components.azure_event_hub
|
||||||
azure-eventhub==5.11.1
|
azure-eventhub==5.11.1
|
||||||
@ -669,7 +669,7 @@ connect-box==0.2.8
|
|||||||
construct==2.10.68
|
construct==2.10.68
|
||||||
|
|
||||||
# homeassistant.components.utility_meter
|
# homeassistant.components.utility_meter
|
||||||
croniter==1.0.6
|
croniter==2.0.2
|
||||||
|
|
||||||
# homeassistant.components.crownstone
|
# homeassistant.components.crownstone
|
||||||
crownstone-cloud==1.4.9
|
crownstone-cloud==1.4.9
|
||||||
@ -1115,7 +1115,7 @@ ibmiotf==0.3.4
|
|||||||
# homeassistant.components.google
|
# homeassistant.components.google
|
||||||
# homeassistant.components.local_calendar
|
# homeassistant.components.local_calendar
|
||||||
# homeassistant.components.local_todo
|
# homeassistant.components.local_todo
|
||||||
ical==7.0.1
|
ical==7.0.3
|
||||||
|
|
||||||
# homeassistant.components.ping
|
# homeassistant.components.ping
|
||||||
icmplib==3.0
|
icmplib==3.0
|
||||||
@ -1971,7 +1971,7 @@ pymitv==1.4.3
|
|||||||
pymochad==0.2.0
|
pymochad==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.modbus
|
# homeassistant.components.modbus
|
||||||
pymodbus==3.6.5
|
pymodbus==3.6.6
|
||||||
|
|
||||||
# homeassistant.components.monoprice
|
# homeassistant.components.monoprice
|
||||||
pymonoprice==0.4
|
pymonoprice==0.4
|
||||||
@ -2090,7 +2090,7 @@ pyrecswitch==1.0.2
|
|||||||
pyrepetierng==0.1.0
|
pyrepetierng==0.1.0
|
||||||
|
|
||||||
# homeassistant.components.risco
|
# homeassistant.components.risco
|
||||||
pyrisco==0.5.8
|
pyrisco==0.5.10
|
||||||
|
|
||||||
# homeassistant.components.rituals_perfume_genie
|
# homeassistant.components.rituals_perfume_genie
|
||||||
pyrituals==0.0.6
|
pyrituals==0.0.6
|
||||||
@ -2155,7 +2155,7 @@ pysmartthings==0.7.8
|
|||||||
pysml==0.0.12
|
pysml==0.0.12
|
||||||
|
|
||||||
# homeassistant.components.snmp
|
# homeassistant.components.snmp
|
||||||
pysnmp-lextudio==6.0.9
|
pysnmp-lextudio==6.0.11
|
||||||
|
|
||||||
# homeassistant.components.snooz
|
# homeassistant.components.snooz
|
||||||
pysnooz==0.8.6
|
pysnooz==0.8.6
|
||||||
@ -2182,7 +2182,7 @@ pyswitchbee==1.8.0
|
|||||||
pytautulli==23.1.1
|
pytautulli==23.1.1
|
||||||
|
|
||||||
# homeassistant.components.tedee
|
# homeassistant.components.tedee
|
||||||
pytedee-async==0.2.16
|
pytedee-async==0.2.17
|
||||||
|
|
||||||
# homeassistant.components.tfiac
|
# homeassistant.components.tfiac
|
||||||
pytfiac==0.4
|
pytfiac==0.4
|
||||||
@ -2340,7 +2340,7 @@ pytrydan==0.4.0
|
|||||||
pyudev==0.23.2
|
pyudev==0.23.2
|
||||||
|
|
||||||
# homeassistant.components.unifiprotect
|
# homeassistant.components.unifiprotect
|
||||||
pyunifiprotect==4.23.3
|
pyunifiprotect==5.0.2
|
||||||
|
|
||||||
# homeassistant.components.uptimerobot
|
# homeassistant.components.uptimerobot
|
||||||
pyuptimerobot==22.2.0
|
pyuptimerobot==22.2.0
|
||||||
|
@ -45,7 +45,7 @@ PlexAPI==4.15.10
|
|||||||
ProgettiHWSW==0.1.3
|
ProgettiHWSW==0.1.3
|
||||||
|
|
||||||
# homeassistant.components.cast
|
# homeassistant.components.cast
|
||||||
PyChromecast==14.0.0
|
PyChromecast==14.0.1
|
||||||
|
|
||||||
# homeassistant.components.flick_electric
|
# homeassistant.components.flick_electric
|
||||||
PyFlick==0.0.2
|
PyFlick==0.0.2
|
||||||
@ -185,7 +185,7 @@ aioaseko==0.0.2
|
|||||||
aioasuswrt==1.4.0
|
aioasuswrt==1.4.0
|
||||||
|
|
||||||
# homeassistant.components.husqvarna_automower
|
# homeassistant.components.husqvarna_automower
|
||||||
aioautomower==2024.3.0
|
aioautomower==2024.3.3
|
||||||
|
|
||||||
# homeassistant.components.azure_devops
|
# homeassistant.components.azure_devops
|
||||||
aioazuredevops==1.3.5
|
aioazuredevops==1.3.5
|
||||||
@ -200,7 +200,7 @@ aiobotocore==2.9.1
|
|||||||
aiocomelit==0.9.0
|
aiocomelit==0.9.0
|
||||||
|
|
||||||
# homeassistant.components.dhcp
|
# homeassistant.components.dhcp
|
||||||
aiodhcpwatcher==0.8.1
|
aiodhcpwatcher==0.8.2
|
||||||
|
|
||||||
# homeassistant.components.dhcp
|
# homeassistant.components.dhcp
|
||||||
aiodiscover==1.6.1
|
aiodiscover==1.6.1
|
||||||
@ -323,7 +323,7 @@ aiopyarr==23.4.0
|
|||||||
aioqsw==0.3.5
|
aioqsw==0.3.5
|
||||||
|
|
||||||
# homeassistant.components.rainforest_raven
|
# homeassistant.components.rainforest_raven
|
||||||
aioraven==0.5.1
|
aioraven==0.5.2
|
||||||
|
|
||||||
# homeassistant.components.recollect_waste
|
# homeassistant.components.recollect_waste
|
||||||
aiorecollect==2023.09.0
|
aiorecollect==2023.09.0
|
||||||
@ -454,7 +454,7 @@ auroranoaa==0.0.3
|
|||||||
aurorapy==0.2.7
|
aurorapy==0.2.7
|
||||||
|
|
||||||
# homeassistant.components.axis
|
# homeassistant.components.axis
|
||||||
axis==54
|
axis==57
|
||||||
|
|
||||||
# homeassistant.components.azure_event_hub
|
# homeassistant.components.azure_event_hub
|
||||||
azure-eventhub==5.11.1
|
azure-eventhub==5.11.1
|
||||||
@ -553,7 +553,7 @@ colorthief==0.2.1
|
|||||||
construct==2.10.68
|
construct==2.10.68
|
||||||
|
|
||||||
# homeassistant.components.utility_meter
|
# homeassistant.components.utility_meter
|
||||||
croniter==1.0.6
|
croniter==2.0.2
|
||||||
|
|
||||||
# homeassistant.components.crownstone
|
# homeassistant.components.crownstone
|
||||||
crownstone-cloud==1.4.9
|
crownstone-cloud==1.4.9
|
||||||
@ -905,7 +905,7 @@ ibeacon-ble==1.2.0
|
|||||||
# homeassistant.components.google
|
# homeassistant.components.google
|
||||||
# homeassistant.components.local_calendar
|
# homeassistant.components.local_calendar
|
||||||
# homeassistant.components.local_todo
|
# homeassistant.components.local_todo
|
||||||
ical==7.0.1
|
ical==7.0.3
|
||||||
|
|
||||||
# homeassistant.components.ping
|
# homeassistant.components.ping
|
||||||
icmplib==3.0
|
icmplib==3.0
|
||||||
@ -1525,7 +1525,7 @@ pymeteoclimatic==0.1.0
|
|||||||
pymochad==0.2.0
|
pymochad==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.modbus
|
# homeassistant.components.modbus
|
||||||
pymodbus==3.6.5
|
pymodbus==3.6.6
|
||||||
|
|
||||||
# homeassistant.components.monoprice
|
# homeassistant.components.monoprice
|
||||||
pymonoprice==0.4
|
pymonoprice==0.4
|
||||||
@ -1617,7 +1617,7 @@ pyqwikswitch==0.93
|
|||||||
pyrainbird==4.0.2
|
pyrainbird==4.0.2
|
||||||
|
|
||||||
# homeassistant.components.risco
|
# homeassistant.components.risco
|
||||||
pyrisco==0.5.8
|
pyrisco==0.5.10
|
||||||
|
|
||||||
# homeassistant.components.rituals_perfume_genie
|
# homeassistant.components.rituals_perfume_genie
|
||||||
pyrituals==0.0.6
|
pyrituals==0.0.6
|
||||||
@ -1673,7 +1673,7 @@ pysmartthings==0.7.8
|
|||||||
pysml==0.0.12
|
pysml==0.0.12
|
||||||
|
|
||||||
# homeassistant.components.snmp
|
# homeassistant.components.snmp
|
||||||
pysnmp-lextudio==6.0.9
|
pysnmp-lextudio==6.0.11
|
||||||
|
|
||||||
# homeassistant.components.snooz
|
# homeassistant.components.snooz
|
||||||
pysnooz==0.8.6
|
pysnooz==0.8.6
|
||||||
@ -1697,7 +1697,7 @@ pyswitchbee==1.8.0
|
|||||||
pytautulli==23.1.1
|
pytautulli==23.1.1
|
||||||
|
|
||||||
# homeassistant.components.tedee
|
# homeassistant.components.tedee
|
||||||
pytedee-async==0.2.16
|
pytedee-async==0.2.17
|
||||||
|
|
||||||
# homeassistant.components.motionmount
|
# homeassistant.components.motionmount
|
||||||
python-MotionMount==0.3.1
|
python-MotionMount==0.3.1
|
||||||
@ -1801,7 +1801,7 @@ pytrydan==0.4.0
|
|||||||
pyudev==0.23.2
|
pyudev==0.23.2
|
||||||
|
|
||||||
# homeassistant.components.unifiprotect
|
# homeassistant.components.unifiprotect
|
||||||
pyunifiprotect==4.23.3
|
pyunifiprotect==5.0.2
|
||||||
|
|
||||||
# homeassistant.components.uptimerobot
|
# homeassistant.components.uptimerobot
|
||||||
pyuptimerobot==22.2.0
|
pyuptimerobot==22.2.0
|
||||||
|
@ -93,10 +93,11 @@ async def test_invalid_hostname(hass: HomeAssistant) -> None:
|
|||||||
assert result["errors"] == {CONF_HOST: "wrong_host"}
|
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."""
|
"""Test connection to host error."""
|
||||||
with patch("brother.Brother.initialize"), patch(
|
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(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
|
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"
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
async def test_zeroconf_snmp_error(hass: HomeAssistant) -> None:
|
@pytest.mark.parametrize("exc", [ConnectionError, TimeoutError, SnmpError("error")])
|
||||||
"""Test we abort zeroconf flow on SNMP error."""
|
async def test_zeroconf_exception(hass: HomeAssistant, exc: Exception) -> None:
|
||||||
|
"""Test we abort zeroconf flow on exception."""
|
||||||
with patch("brother.Brother.initialize"), patch(
|
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(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch
|
|||||||
from aioautomower.model import MowerModes
|
from aioautomower.model import MowerModes
|
||||||
from aioautomower.utils import mower_list_to_dictionary_dataclass
|
from aioautomower.utils import mower_list_to_dictionary_dataclass
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
import pytest
|
||||||
from syrupy import SnapshotAssertion
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.components.husqvarna_automower.const import DOMAIN
|
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 is not None
|
||||||
assert state.state == "0.034"
|
assert state.state == "0.034"
|
||||||
|
|
||||||
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
|
||||||
await hass.config_entries.async_remove(entry.entry_id)
|
@pytest.mark.parametrize(
|
||||||
await hass.async_block_till_done()
|
("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(
|
values = mower_list_to_dictionary_dataclass(
|
||||||
load_json_value_fixture("mower.json", DOMAIN)
|
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
|
mock_automower_client.get_status.return_value = values
|
||||||
await setup_integration(hass, mock_config_entry)
|
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
|
assert state is None
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,6 +22,16 @@
|
|||||||
list([
|
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]
|
# name: test_parse_existing_ics[migrate_legacy_due]
|
||||||
list([
|
list([
|
||||||
dict({
|
dict({
|
||||||
|
@ -671,6 +671,28 @@ async def test_move_item_previous_unknown(
|
|||||||
),
|
),
|
||||||
"1",
|
"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=(
|
ids=(
|
||||||
"empty",
|
"empty",
|
||||||
@ -679,6 +701,7 @@ async def test_move_item_previous_unknown(
|
|||||||
"needs_action",
|
"needs_action",
|
||||||
"migrate_legacy_due",
|
"migrate_legacy_due",
|
||||||
"due",
|
"due",
|
||||||
|
"invalid_dtstart_tzname",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def test_parse_existing_ics(
|
async def test_parse_existing_ics(
|
||||||
|
@ -155,7 +155,9 @@ MOCK_OWPROXY_DEVICES = {
|
|||||||
{ATTR_INJECT_READS: b" 1"},
|
{ATTR_INJECT_READS: b" 1"},
|
||||||
{ATTR_INJECT_READS: b" 0"},
|
{ATTR_INJECT_READS: b" 0"},
|
||||||
{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"},
|
{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" 1"},
|
||||||
{ATTR_INJECT_READS: b" 0"},
|
{ATTR_INJECT_READS: b" 0"},
|
||||||
{ATTR_INJECT_READS: b" 1"},
|
{ATTR_INJECT_READS: b" 1"},
|
||||||
{ATTR_INJECT_READS: b" 0"},
|
{
|
||||||
|
ATTR_INJECT_READS: ProtocolError,
|
||||||
|
},
|
||||||
{ATTR_INJECT_READS: b" 1"},
|
{ATTR_INJECT_READS: b" 1"},
|
||||||
{ATTR_INJECT_READS: b" 0"},
|
{ATTR_INJECT_READS: b" 0"},
|
||||||
{ATTR_INJECT_READS: b" 1"},
|
{ATTR_INJECT_READS: b" 1"},
|
||||||
|
@ -851,13 +851,13 @@
|
|||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
'device_file': '/29.111111111111/sensed.3',
|
'device_file': '/29.111111111111/sensed.3',
|
||||||
'friendly_name': '29.111111111111 Sensed 3',
|
'friendly_name': '29.111111111111 Sensed 3',
|
||||||
'raw_value': 0.0,
|
'raw_value': None,
|
||||||
}),
|
}),
|
||||||
'context': <ANY>,
|
'context': <ANY>,
|
||||||
'entity_id': 'binary_sensor.29_111111111111_sensed_3',
|
'entity_id': 'binary_sensor.29_111111111111_sensed_3',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': 'off',
|
'state': 'unknown',
|
||||||
}),
|
}),
|
||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
|
@ -1271,13 +1271,13 @@
|
|||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
'device_file': '/29.111111111111/PIO.3',
|
'device_file': '/29.111111111111/PIO.3',
|
||||||
'friendly_name': '29.111111111111 Programmed input-output 3',
|
'friendly_name': '29.111111111111 Programmed input-output 3',
|
||||||
'raw_value': 0.0,
|
'raw_value': None,
|
||||||
}),
|
}),
|
||||||
'context': <ANY>,
|
'context': <ANY>,
|
||||||
'entity_id': 'switch.29_111111111111_programmed_input_output_3',
|
'entity_id': 'switch.29_111111111111_programmed_input_output_3',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': 'off',
|
'state': 'unknown',
|
||||||
}),
|
}),
|
||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
|
@ -171,16 +171,6 @@ def connect_with_error(exception):
|
|||||||
yield
|
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
|
@pytest.fixture
|
||||||
async def setup_risco_local(hass, local_config_entry):
|
async def setup_risco_local(hass, local_config_entry):
|
||||||
"""Set up a local Risco integration for testing."""
|
"""Set up a local Risco integration for testing."""
|
||||||
|
@ -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
|
|
@ -67,6 +67,18 @@ async def test_block_reload_on_cfg_change(
|
|||||||
mock_block_device.mock_update()
|
mock_block_device.mock_update()
|
||||||
await hass.async_block_till_done()
|
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
|
# Generate config change from switch to light
|
||||||
monkeypatch.setitem(
|
monkeypatch.setitem(
|
||||||
mock_block_device.settings["relays"][RELAY_BLOCK_ID], "appliance_type", "light"
|
mock_block_device.settings["relays"][RELAY_BLOCK_ID], "appliance_type", "light"
|
||||||
|
@ -29,6 +29,7 @@ import homeassistant.util.dt as dt_util
|
|||||||
from tests.common import (
|
from tests.common import (
|
||||||
MockConfigEntry,
|
MockConfigEntry,
|
||||||
assert_setup_component,
|
assert_setup_component,
|
||||||
|
async_capture_events,
|
||||||
async_fire_time_changed,
|
async_fire_time_changed,
|
||||||
mock_restore_cache_with_extra_data,
|
mock_restore_cache_with_extra_data,
|
||||||
)
|
)
|
||||||
@ -1848,6 +1849,7 @@ async def test_trigger_entity_restore_state(
|
|||||||
"my_variable": "{{ trigger.event.data.beer + 1 }}"
|
"my_variable": "{{ trigger.event.data.beer + 1 }}"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{"event": "test_event2", "event_data": {"hello": "world"}},
|
||||||
],
|
],
|
||||||
"sensor": [
|
"sensor": [
|
||||||
{
|
{
|
||||||
@ -1864,6 +1866,10 @@ async def test_trigger_action(
|
|||||||
hass: HomeAssistant, start_ha, entity_registry: er.EntityRegistry
|
hass: HomeAssistant, start_ha, entity_registry: er.EntityRegistry
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test trigger entity with an action works."""
|
"""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")
|
state = hass.states.get("sensor.hello_name")
|
||||||
assert state is not None
|
assert state is not None
|
||||||
assert state.state == STATE_UNKNOWN
|
assert state.state == STATE_UNKNOWN
|
||||||
@ -1875,3 +1881,6 @@ async def test_trigger_action(
|
|||||||
state = hass.states.get("sensor.hello_name")
|
state = hass.states.get("sensor.hello_name")
|
||||||
assert state.state == "3"
|
assert state.state == "3"
|
||||||
assert state.context is context
|
assert state.context is context
|
||||||
|
|
||||||
|
assert len(events) == 1
|
||||||
|
assert events[0].context.parent_id == context.id
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
'uniqueId': 'abc123',
|
'uniqueId': 'abc123',
|
||||||
}),
|
}),
|
||||||
'geofence': dict({
|
'geofence': dict({
|
||||||
'area': 'string',
|
'area': '**REDACTED**',
|
||||||
'attributes': dict({
|
'attributes': dict({
|
||||||
}),
|
}),
|
||||||
'calendarId': 0,
|
'calendarId': 0,
|
||||||
@ -134,7 +134,7 @@
|
|||||||
'uniqueId': 'abc123',
|
'uniqueId': 'abc123',
|
||||||
}),
|
}),
|
||||||
'geofence': dict({
|
'geofence': dict({
|
||||||
'area': 'string',
|
'area': '**REDACTED**',
|
||||||
'attributes': dict({
|
'attributes': dict({
|
||||||
}),
|
}),
|
||||||
'calendarId': 0,
|
'calendarId': 0,
|
||||||
|
@ -318,7 +318,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) -
|
|||||||
"disable_rtsp": True,
|
"disable_rtsp": True,
|
||||||
"override_connection_host": True,
|
"override_connection_host": True,
|
||||||
"max_media": 1000,
|
"max_media": 1000,
|
||||||
"allow_ea": False,
|
"allow_ea_channel": False,
|
||||||
}
|
}
|
||||||
await hass.config_entries.async_unload(mock_config.entry_id)
|
await hass.config_entries.async_unload(mock_config.entry_id)
|
||||||
|
|
||||||
|
@ -45,12 +45,14 @@ async def test_ea_warning_ignore(
|
|||||||
assert len(msg["result"]["issues"]) > 0
|
assert len(msg["result"]["issues"]) > 0
|
||||||
issue = None
|
issue = None
|
||||||
for i in msg["result"]["issues"]:
|
for i in msg["result"]["issues"]:
|
||||||
if i["issue_id"] == "ea_warning":
|
if i["issue_id"] == "ea_channel_warning":
|
||||||
issue = i
|
issue = i
|
||||||
assert issue is not None
|
assert issue is not None
|
||||||
|
|
||||||
url = RepairsFlowIndexView.url
|
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
|
assert resp.status == HTTPStatus.OK
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
|
|
||||||
@ -103,12 +105,14 @@ async def test_ea_warning_fix(
|
|||||||
assert len(msg["result"]["issues"]) > 0
|
assert len(msg["result"]["issues"]) > 0
|
||||||
issue = None
|
issue = None
|
||||||
for i in msg["result"]["issues"]:
|
for i in msg["result"]["issues"]:
|
||||||
if i["issue_id"] == "ea_warning":
|
if i["issue_id"] == "ea_channel_warning":
|
||||||
issue = i
|
issue = i
|
||||||
assert issue is not None
|
assert issue is not None
|
||||||
|
|
||||||
url = RepairsFlowIndexView.url
|
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
|
assert resp.status == HTTPStatus.OK
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
|
|
||||||
@ -121,8 +125,9 @@ async def test_ea_warning_fix(
|
|||||||
|
|
||||||
new_nvr = copy(ufp.api.bootstrap.nvr)
|
new_nvr = copy(ufp.api.bootstrap.nvr)
|
||||||
new_nvr.version = Version("2.2.6")
|
new_nvr.version = Version("2.2.6")
|
||||||
|
new_nvr.release_channel = "release"
|
||||||
mock_msg = Mock()
|
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
|
mock_msg.new_obj = new_nvr
|
||||||
|
|
||||||
ufp.api.bootstrap.nvr = new_nvr
|
ufp.api.bootstrap.nvr = new_nvr
|
||||||
|
Loading…
x
Reference in New Issue
Block a user