Skip to content
Merged

2024.3.2 #113973

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
10fc40e
Streamline Notion config entry updates (refresh token and user ID) (#…
bachya Mar 10, 2024
8b00229
Bump aioautomower to 2024.3.2 (#113162)
Thomas55555 Mar 12, 2024
a167b0a
Bump aioautomower to 2024.3.3 (#113430)
Thomas55555 Mar 14, 2024
273d01c
Check for EA release channel for UniFi Protect (#113432)
AngellusMortis Mar 14, 2024
26b26a3
Bump `pysnmp-lextudio` to version `6.0.11` (#113463)
lextm Mar 15, 2024
a5994d1
Tado fix water heater (#113464)
erwindouna Mar 20, 2024
099c228
Bump aiodhcpwatcher to 0.8.2 (#113466)
bdraco Mar 14, 2024
de966b0
Bump axis to v55 (#113479)
Kane610 Mar 15, 2024
8bae8fd
Bump croniter to 2.0.2 (#113494)
dgomes Mar 15, 2024
5163b5f
Revert setting communication delay in Risco init (#113497)
OnFreund Mar 15, 2024
05b9003
Bump pyrisco to 0.5.10 (#113505)
OnFreund Mar 15, 2024
d5864a4
Fix missing context when running script from template entity (#113523)
emontnemery Mar 16, 2024
4a620e0
Bump ical to 7.0.3 to fix local-todo persisted with invalid DTSTART v…
allenporter Mar 16, 2024
a7908d8
Fix Airthings BLE illuminance sensor name (#113560)
joostlek Mar 15, 2024
0a64ae2
Ignore Shelly block update with cfgChanged None (#113587)
thecode Mar 16, 2024
2e2d303
Catch `TimeoutError` in `Brother` config flow (#113593)
bieniu Mar 16, 2024
fa9f5bd
Bump axis to v56 (#113608)
Kane610 Mar 16, 2024
686487e
Bump pyunifiprotect to 5.0.1 (#113630)
AngellusMortis Mar 16, 2024
eb8a842
Bump pyunifiprotect to 5.0.2 (#113651)
AngellusMortis Mar 17, 2024
368586c
Add removal condition to Shelly battery sensor (#113703)
bieniu Mar 17, 2024
6859bae
Bump aioraven to 0.5.2 (#113714)
cottsay Mar 20, 2024
33678ff
Fix unknown values in onewire (#113731)
epenet Mar 18, 2024
d67cd2a
Bump pymodbus v3.6.6 (#113796)
janiversen Mar 19, 2024
4132a3d
Catch API errors in cast media_player service handlers (#113839)
emontnemery Mar 20, 2024
14c4cdc
Bump pychromecast to 14.0.1 (#113841)
emontnemery Mar 19, 2024
8056886
Fix startup race in cast (#113843)
emontnemery Mar 19, 2024
1e57f52
Redact the area of traccar server geofences (#113861)
ludeeus Mar 20, 2024
6c274ab
Bump pytedee_async to 0.2.17 (#113933)
zweckj Mar 21, 2024
19ef927
Bump axis to v57 (#113952)
Kane610 Mar 21, 2024
91bb321
Bump version to 2024.3.2
balloob Mar 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions homeassistant/components/airthings_ble/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
),
"illuminance": SensorEntityDescription(
key="illuminance",
translation_key="illuminance",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/airthings_ble/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
},
"radon_longterm_level": {
"name": "Radon longterm level"
},
"illuminance": {
"name": "[%key:component::sensor::entity_component::illuminance::name%]"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/axis/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"iot_class": "local_push",
"loggers": ["axis"],
"quality_scale": "platinum",
"requirements": ["axis==54"],
"requirements": ["axis==57"],
"ssdp": [
{
"manufacturer": "AXIS"
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/brother/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ async def async_step_user(
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"
Expand Down Expand Up @@ -88,7 +88,7 @@ async def async_step_zeroconf(
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
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/cast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@

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

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/cast/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."]
}
68 changes: 61 additions & 7 deletions homeassistant/components/cast/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
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
Expand All @@ -18,6 +19,7 @@
)
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,
Expand Down Expand Up @@ -83,6 +85,34 @@
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.
Expand Down Expand Up @@ -476,6 +506,21 @@ def _media_controller(self):

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."""

Expand All @@ -486,52 +531,61 @@ def turn_on(self) -> None:

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()
Expand Down Expand Up @@ -644,7 +698,7 @@ async def async_play_media(
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",
Expand All @@ -655,7 +709,7 @@ async def async_play_media(
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)
Expand Down Expand Up @@ -729,7 +783,7 @@ async def async_play_media(
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):
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/dhcp/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/google/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/husqvarna_automower/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
7 changes: 7 additions & 0 deletions homeassistant/components/husqvarna_automower/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription):
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(
Expand All @@ -79,6 +80,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription):
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(
Expand All @@ -89,6 +91,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription):
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(
Expand All @@ -99,6 +102,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription):
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(
Expand All @@ -107,6 +111,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription):
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(
Expand All @@ -115,6 +120,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription):
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(
Expand All @@ -125,6 +131,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription):
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(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/local_calendar/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/local_todo/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/modbus/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pymodbus"],
"quality_scale": "gold",
"requirements": ["pymodbus==3.6.5"]
"requirements": ["pymodbus==3.6.6"]
}
19 changes: 10 additions & 9 deletions homeassistant/components/notion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -180,12 +187,6 @@ def async_save_refresh_token(refresh_token: str) -> None:
# 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)
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/onewire/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
6 changes: 4 additions & 2 deletions homeassistant/components/onewire/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/rainforest_raven/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading