Compare commits

..

6 Commits

Author SHA1 Message Date
Erik b2fd8836c9 Enable disabled devices when adding new config entry 2024-06-27 08:23:40 +02:00
Robert Resch 31e9de3b95 Adapt Roborock to runtime_data (#120578)
* Adopt Roborock to runtime_data

* Fix name
2024-06-26 19:42:15 +02:00
Shay Levy ed1eb8ac9c Change Shelly connect task log message level to error (#120582) 2024-06-26 18:19:28 +02:00
TheJulianJES d09a274548 Bump ZHA dependencies (#120581) 2024-06-26 18:15:53 +02:00
Michael Hansen 02b142fbde Bump intents to 2024.6.26 (#120584)
Bump intents
2024-06-26 11:13:01 -05:00
Franck Nijhof 33b4f40b2a Bump version to 2024.8.0dev0 (#120577) 2024-06-26 16:55:08 +02:00
42 changed files with 145 additions and 215 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ env:
CACHE_VERSION: 9
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 8
HA_SHORT_VERSION: "2024.7"
HA_SHORT_VERSION: "2024.8"
DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12']"
# 10.3 is the oldest supported version
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"loggers": ["adguardhome"],
"requirements": ["adguardhome==0.7.0"]
"requirements": ["adguardhome==0.6.3"]
}
@@ -45,7 +45,7 @@ from homeassistant.components.media_player import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MODEL
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -316,7 +316,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
@callback
def _async_update_playback_error(self, data: PlaybackError) -> None:
"""Show playback error."""
raise HomeAssistantError(data.error)
_LOGGER.error(data.error)
@callback
def _async_update_playback_progress(self, data: PlaybackProgress) -> None:
@@ -516,9 +516,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
self.async_write_ha_state()
else:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="non_deezer_seeking"
)
_LOGGER.error("Seeking is currently only supported when using Deezer")
async def async_media_previous_track(self) -> None:
"""Send the previous track command."""
@@ -531,14 +529,12 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
async def async_select_source(self, source: str) -> None:
"""Select an input source."""
if source not in self._sources.values():
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_source",
translation_placeholders={
"invalid_source": source,
"valid_sources": ",".join(list(self._sources.values())),
},
_LOGGER.error(
"Invalid source: %s. Valid sources are: %s",
source,
list(self._sources.values()),
)
return
key = [x for x in self._sources if self._sources[x] == source][0]
@@ -563,14 +559,12 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
media_type = MediaType.MUSIC
if media_type not in VALID_MEDIA_TYPES:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_media_type",
translation_placeholders={
"invalid_media_type": media_type,
"valid_media_types": ",".join(VALID_MEDIA_TYPES),
},
_LOGGER.error(
"%s is an invalid type. Valid values are: %s",
media_type,
VALID_MEDIA_TYPES,
)
return
if media_source.is_media_source_id(media_id):
sourced_media = await media_source.async_resolve_media(
@@ -687,14 +681,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
)
except ApiException as error:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="play_media_error",
translation_placeholders={
"media_type": media_type,
"error_message": json.loads(error.body)["message"],
},
) from error
_LOGGER.error(json.loads(error.body)["message"])
async def async_browse_media(
self,
@@ -28,18 +28,6 @@
"exceptions": {
"m3u_invalid_format": {
"message": "Media sources with the .m3u extension are not supported."
},
"non_deezer_seeking": {
"message": "Seeking is currently only supported when using Deezer"
},
"invalid_source": {
"message": "Invalid source: {invalid_source}. Valid sources are: {valid_sources}"
},
"invalid_media_type": {
"message": "{invalid_media_type} is an invalid type. Valid values are: {valid_media_types}."
},
"play_media_error": {
"message": "An error occurred while attempting to play {media_type}: {error_message}."
}
}
}
@@ -81,9 +81,6 @@
}
},
"exceptions": {
"manual_switching_disabled": {
"message": "Can't toggle switch while manual switching is disabled for the device."
},
"change_preset_while_active_mode": {
"message": "Can't change preset while holiday or summer mode is active on the device."
},
@@ -6,11 +6,9 @@ from typing import Any
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxDeviceEntity
from .const import DOMAIN
from .coordinator import FritzboxConfigEntry
@@ -50,20 +48,10 @@ class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
self.check_lock_state()
await self.hass.async_add_executor_job(self.data.set_switch_state_on)
await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
self.check_lock_state()
await self.hass.async_add_executor_job(self.data.set_switch_state_off)
await self.coordinator.async_refresh()
def check_lock_state(self) -> None:
"""Raise an Error if manual switching via FRITZ!Box user interface is disabled."""
if self.data.lock:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="manual_switching_disabled",
)
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20240626.2"]
"requirements": ["home-assistant-frontend==20240626.0"]
}
+1 -1
View File
@@ -483,7 +483,7 @@ class HomeAssistantHTTP:
frame.report(
"calls hass.http.register_static_path which is deprecated because "
"it does blocking I/O in the event loop, instead "
"call `await hass.http.async_register_static_paths("
"call `await hass.http.async_register_static_path("
f'[StaticPathConfig("{url_path}", "{path}", {cache_headers})])`; '
"This function will be removed in 2025.7",
exclude_integrations={"http"},
@@ -446,6 +446,7 @@ class IntegrationSensor(RestoreSensor):
event_filter=callback(
lambda event_data: event_data["entity_id"] == self._sensor_source_id
),
run_immediately=True,
)
)
self.async_on_remove(
@@ -455,6 +456,7 @@ class IntegrationSensor(RestoreSensor):
event_filter=callback(
lambda event_data: event_data["entity_id"] == self._sensor_source_id
),
run_immediately=True,
)
)
+16 -11
View File
@@ -28,6 +28,8 @@ SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__)
type RoborockConfigEntry = ConfigEntry[RoborockCoordinators]
@dataclass
class RoborockCoordinators:
@@ -43,7 +45,7 @@ class RoborockCoordinators:
return self.v1 + self.a01
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> bool:
"""Set up roborock from a config entry."""
_LOGGER.debug("Integration async setup entry: %s", entry.as_dict())
@@ -99,7 +101,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
translation_key="no_coordinators",
)
valid_coordinators = RoborockCoordinators(v1_coords, a01_coords)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = valid_coordinators
async def on_unload() -> None:
release_tasks = set()
for coordinator in valid_coordinators.values():
release_tasks.add(coordinator.release())
await asyncio.gather(*release_tasks)
entry.async_on_unload(on_unload)
entry.runtime_data = valid_coordinators
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -231,18 +242,12 @@ async def setup_device_a01(
return coord
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> bool:
"""Handle removal of an entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
release_tasks = set()
for coordinator in hass.data[DOMAIN][entry.entry_id].values():
release_tasks.add(coordinator.release())
hass.data[DOMAIN].pop(entry.entry_id)
await asyncio.gather(*release_tasks)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def update_listener(hass: HomeAssistant, entry: RoborockConfigEntry) -> None:
"""Handle options update."""
# Reload entry to update data
await hass.config_entries.async_reload(entry.entry_id)
@@ -12,14 +12,12 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
from . import RoborockCoordinators
from .const import DOMAIN
from . import RoborockConfigEntry
from .coordinator import RoborockDataUpdateCoordinator
from .device import RoborockCoordinatedEntityV1
@@ -72,17 +70,16 @@ BINARY_SENSOR_DESCRIPTIONS = [
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RoborockConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Roborock vacuum binary sensors."""
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities(
RoborockBinarySensorEntity(
coordinator,
description,
)
for coordinator in coordinators.v1
for coordinator in config_entry.runtime_data.v1
for description in BINARY_SENSOR_DESCRIPTIONS
if description.value_fn(coordinator.roborock_device_info.props) is not None
)
+3 -6
View File
@@ -7,14 +7,12 @@ from dataclasses import dataclass
from roborock.roborock_typing import RoborockCommand
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
from . import RoborockCoordinators
from .const import DOMAIN
from . import RoborockConfigEntry
from .coordinator import RoborockDataUpdateCoordinator
from .device import RoborockEntityV1
@@ -65,17 +63,16 @@ CONSUMABLE_BUTTON_DESCRIPTIONS = [
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RoborockConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Roborock button platform."""
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities(
RoborockButtonEntity(
coordinator,
description,
)
for coordinator in coordinators.v1
for coordinator in config_entry.runtime_data.v1
for description in CONSUMABLE_BUTTON_DESCRIPTIONS
if isinstance(coordinator, RoborockDataUpdateCoordinator)
)
@@ -5,12 +5,10 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics.util import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant
from . import RoborockCoordinators
from .const import DOMAIN
from . import RoborockConfigEntry
TO_REDACT_CONFIG = ["token", "sn", "rruid", CONF_UNIQUE_ID, "username", "uid"]
@@ -18,10 +16,10 @@ TO_REDACT_COORD = ["duid", "localKey", "mac", "bssid"]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
hass: HomeAssistant, config_entry: RoborockConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
coordinators = config_entry.runtime_data
return {
"config_entry": async_redact_data(config_entry.data, TO_REDACT_CONFIG),
+3 -5
View File
@@ -13,7 +13,6 @@ from vacuum_map_parser_base.config.size import Sizes
from vacuum_map_parser_roborock.map_data_parser import RoborockMapDataParser
from homeassistant.components.image import ImageEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -21,7 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
import homeassistant.util.dt as dt_util
from . import RoborockCoordinators
from . import RoborockConfigEntry
from .const import DEFAULT_DRAWABLES, DOMAIN, DRAWABLES, IMAGE_CACHE_INTERVAL, MAP_SLEEP
from .coordinator import RoborockDataUpdateCoordinator
from .device import RoborockCoordinatedEntityV1
@@ -29,12 +28,11 @@ from .device import RoborockCoordinatedEntityV1
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RoborockConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Roborock image platform."""
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
drawables = [
drawable
for drawable, default_value in DEFAULT_DRAWABLES.items()
@@ -45,7 +43,7 @@ async def async_setup_entry(
await asyncio.gather(
*(
create_coordinator_maps(coord, drawables)
for coord in coordinators.v1
for coord in config_entry.runtime_data.v1
)
)
)
+3 -6
View File
@@ -11,14 +11,12 @@ from roborock.exceptions import RoborockException
from roborock.version_1_apis.roborock_client_v1 import AttributeCache
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
from . import RoborockCoordinators
from .const import DOMAIN
from . import RoborockConfigEntry
from .coordinator import RoborockDataUpdateCoordinator
from .device import RoborockEntityV1
@@ -51,16 +49,15 @@ NUMBER_DESCRIPTIONS: list[RoborockNumberDescription] = [
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RoborockConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Roborock number platform."""
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
possible_entities: list[
tuple[RoborockDataUpdateCoordinator, RoborockNumberDescription]
] = [
(coordinator, description)
for coordinator in coordinators.v1
for coordinator in config_entry.runtime_data.v1
for description in NUMBER_DESCRIPTIONS
]
# We need to check if this function is supported by the device.
+3 -6
View File
@@ -8,14 +8,12 @@ from roborock.roborock_message import RoborockDataProtocol
from roborock.roborock_typing import RoborockCommand
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
from . import RoborockCoordinators
from .const import DOMAIN
from . import RoborockConfigEntry
from .coordinator import RoborockDataUpdateCoordinator
from .device import RoborockCoordinatedEntityV1
@@ -65,15 +63,14 @@ SELECT_DESCRIPTIONS: list[RoborockSelectDescription] = [
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RoborockConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Roborock select platform."""
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities(
RoborockSelectEntity(coordinator, description, options)
for coordinator in coordinators.v1
for coordinator in config_entry.runtime_data.v1
for description in SELECT_DESCRIPTIONS
if (
options := description.options_lambda(
+3 -5
View File
@@ -21,7 +21,6 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
AREA_SQUARE_METERS,
PERCENTAGE,
@@ -33,8 +32,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util import slugify
from . import RoborockCoordinators
from .const import DOMAIN
from . import RoborockConfigEntry
from .coordinator import RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01
from .device import RoborockCoordinatedEntityA01, RoborockCoordinatedEntityV1
@@ -255,11 +253,11 @@ A01_SENSOR_DESCRIPTIONS: list[RoborockSensorDescriptionA01] = [
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RoborockConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Roborock vacuum sensors."""
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
coordinators = config_entry.runtime_data
async_add_entities(
RoborockSensorEntity(
coordinator,
+3 -6
View File
@@ -12,14 +12,12 @@ from roborock.command_cache import CacheableAttribute
from roborock.version_1_apis.roborock_client_v1 import AttributeCache
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
from . import RoborockCoordinators
from .const import DOMAIN
from . import RoborockConfigEntry
from .coordinator import RoborockDataUpdateCoordinator
from .device import RoborockEntityV1
@@ -99,16 +97,15 @@ SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RoborockConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Roborock switch platform."""
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
possible_entities: list[
tuple[RoborockDataUpdateCoordinator, RoborockSwitchDescription]
] = [
(coordinator, description)
for coordinator in coordinators.v1
for coordinator in config_entry.runtime_data.v1
for description in SWITCH_DESCRIPTIONS
]
# We need to check if this function is supported by the device.
+3 -6
View File
@@ -13,14 +13,12 @@ from roborock.exceptions import RoborockException
from roborock.version_1_apis.roborock_client_v1 import AttributeCache
from homeassistant.components.time import TimeEntity, TimeEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
from . import RoborockCoordinators
from .const import DOMAIN
from . import RoborockConfigEntry
from .coordinator import RoborockDataUpdateCoordinator
from .device import RoborockEntityV1
@@ -115,16 +113,15 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RoborockConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Roborock time platform."""
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
possible_entities: list[
tuple[RoborockDataUpdateCoordinator, RoborockTimeDescription]
] = [
(coordinator, description)
for coordinator in coordinators.v1
for coordinator in config_entry.runtime_data.v1
for description in TIME_DESCRIPTIONS
]
# We need to check if this function is supported by the device.
+3 -5
View File
@@ -17,13 +17,12 @@ from homeassistant.components.vacuum import (
StateVacuumEntity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
from . import RoborockCoordinators
from . import RoborockConfigEntry
from .const import DOMAIN, GET_MAPS_SERVICE_NAME
from .coordinator import RoborockDataUpdateCoordinator
from .device import RoborockCoordinatedEntityV1
@@ -57,14 +56,13 @@ STATE_CODE_TO_STATE = {
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RoborockConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Roborock sensor."""
coordinators: RoborockCoordinators = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities(
RoborockVacuum(coordinator)
for coordinator in coordinators.v1
for coordinator in config_entry.runtime_data.v1
if isinstance(coordinator, RoborockDataUpdateCoordinator)
)
+3 -1
View File
@@ -83,9 +83,11 @@ REST_SENSORS_UPDATE_INTERVAL: Final = 60
# Refresh interval for RPC polling sensors
RPC_SENSORS_POLLING_INTERVAL: Final = 60
# Multiplier used to calculate the "update_interval" for sleeping devices.
SLEEP_PERIOD_MULTIPLIER: Final = 1.2
CONF_SLEEP_PERIOD: Final = "sleep_period"
# Multiplier used to calculate the "update_interval" for shelly devices.
# Multiplier used to calculate the "update_interval" for non-sleeping devices.
UPDATE_PERIOD_MULTIPLIER: Final = 2.2
# Reconnect interval for GEN2 devices
@@ -54,6 +54,7 @@ from .const import (
RPC_RECONNECT_INTERVAL,
RPC_SENSORS_POLLING_INTERVAL,
SHBTN_MODELS,
SLEEP_PERIOD_MULTIPLIER,
UPDATE_PERIOD_MULTIPLIER,
BLEScannerMode,
)
@@ -228,7 +229,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]):
"""Initialize the Shelly block device coordinator."""
self.entry = entry
if self.sleep_period:
update_interval = UPDATE_PERIOD_MULTIPLIER * self.sleep_period
update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period
else:
update_interval = (
UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"]
@@ -428,7 +429,7 @@ class ShellyRestCoordinator(ShellyCoordinatorBase[BlockDevice]):
in BATTERY_DEVICES_WITH_PERMANENT_CONNECTION
):
update_interval = (
UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"]
SLEEP_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"]
)
super().__init__(hass, entry, device, update_interval)
@@ -458,7 +459,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
"""Initialize the Shelly RPC device coordinator."""
self.entry = entry
if self.sleep_period:
update_interval = UPDATE_PERIOD_MULTIPLIER * self.sleep_period
update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period
else:
update_interval = RPC_RECONNECT_INTERVAL
super().__init__(hass, entry, device, update_interval)
@@ -485,7 +486,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
data[CONF_SLEEP_PERIOD] = wakeup_period
self.hass.config_entries.async_update_entry(self.entry, data=data)
update_interval = UPDATE_PERIOD_MULTIPLIER * wakeup_period
update_interval = SLEEP_PERIOD_MULTIPLIER * wakeup_period
self.update_interval = timedelta(seconds=update_interval)
return True
@@ -24,8 +24,6 @@ async def async_get_config_entry_diagnostics(
device_settings: str | dict = "not initialized"
device_status: str | dict = "not initialized"
bluetooth: str | dict = "not initialized"
last_error: str = "not initialized"
if shelly_entry_data.block:
block_coordinator = shelly_entry_data.block
assert block_coordinator
@@ -57,10 +55,6 @@ async def async_get_config_entry_diagnostics(
"uptime",
]
}
if block_coordinator.device.last_error:
last_error = repr(block_coordinator.device.last_error)
else:
rpc_coordinator = shelly_entry_data.rpc
assert rpc_coordinator
@@ -85,9 +79,6 @@ async def async_get_config_entry_diagnostics(
"scanner": await scanner.async_diagnostics(),
}
if rpc_coordinator.device.last_error:
last_error = repr(rpc_coordinator.device.last_error)
if isinstance(device_status, dict):
device_status = async_redact_data(device_status, ["ssid"])
@@ -96,6 +87,5 @@ async def async_get_config_entry_diagnostics(
"device_info": device_info,
"device_settings": device_settings,
"device_status": device_status,
"last_error": last_error,
"bluetooth": bluetooth,
}
@@ -702,7 +702,7 @@ class TelegramNotificationService:
}
if message_tag is not None:
event_data[ATTR_MESSAGE_TAG] = message_tag
if kwargs_msg.get(ATTR_MESSAGE_THREAD_ID) is not None:
if kwargs_msg[ATTR_MESSAGE_THREAD_ID] is not None:
event_data[ATTR_MESSAGE_THREAD_ID] = kwargs_msg[
ATTR_MESSAGE_THREAD_ID
]
+2 -2
View File
@@ -23,8 +23,8 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 7
PATCH_VERSION: Final = "0b2"
MINOR_VERSION: Final = 8
PATCH_VERSION: Final = "0.dev0"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
+6
View File
@@ -926,6 +926,12 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
if add_config_entry.entry_id not in old.config_entries:
config_entries = old.config_entries | {add_config_entry.entry_id}
if (
old.disabled_by
and not add_config_entry.disabled_by
and disabled_by is UNDEFINED
):
disabled_by = None
if (
remove_config_entry_id is not UNDEFINED
+1 -1
View File
@@ -32,7 +32,7 @@ habluetooth==3.1.3
hass-nabucasa==0.81.1
hassil==1.7.1
home-assistant-bluetooth==1.12.2
home-assistant-frontend==20240626.2
home-assistant-frontend==20240626.0
home-assistant-intents==2024.6.26
httpx==0.27.0
ifaddr==0.2.0
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2024.7.0b2"
version = "2024.8.0.dev0"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
+2 -2
View File
@@ -149,7 +149,7 @@ adb-shell[async]==0.4.4
adext==0.4.3
# homeassistant.components.adguard
adguardhome==0.7.0
adguardhome==0.6.3
# homeassistant.components.advantage_air
advantage-air==0.4.4
@@ -1090,7 +1090,7 @@ hole==0.8.0
holidays==0.51
# homeassistant.components.frontend
home-assistant-frontend==20240626.2
home-assistant-frontend==20240626.0
# homeassistant.components.conversation
home-assistant-intents==2024.6.26
+2 -2
View File
@@ -128,7 +128,7 @@ adb-shell[async]==0.4.4
adext==0.4.3
# homeassistant.components.adguard
adguardhome==0.7.0
adguardhome==0.6.3
# homeassistant.components.advantage_air
advantage-air==0.4.4
@@ -895,7 +895,7 @@ hole==0.8.0
holidays==0.51
# homeassistant.components.frontend
home-assistant-frontend==20240626.2
home-assistant-frontend==20240626.0
# homeassistant.components.conversation
home-assistant-intents==2024.6.26
@@ -92,7 +92,7 @@ async def test_api_error(hass: HomeAssistant) -> None:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data=deepcopy(FIXTURE_USER_INPUT),
data=FIXTURE_USER_INPUT,
)
assert result["type"] is FlowResultType.FORM
@@ -116,7 +116,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None:
result2 = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data=deepcopy(FIXTURE_USER_INPUT),
data=FIXTURE_USER_INPUT,
)
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == FIXTURE_COMPLETE_ENTRY[CONF_USERNAME]
@@ -137,8 +137,7 @@ async def test_options_flow_implementation(hass: HomeAssistant) -> None:
return_value=True,
) as mock_setup_entry,
):
config_entry_args = deepcopy(FIXTURE_CONFIG_ENTRY)
config_entry = MockConfigEntry(**config_entry_args)
config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
@@ -1,6 +1,5 @@
"""Test Axis component setup process."""
from copy import deepcopy
from unittest.mock import patch
import pytest
@@ -38,7 +37,7 @@ async def test_migrate_options(
) -> None:
"""Test successful migration of options."""
config_entry = deepcopy(FIXTURE_CONFIG_ENTRY)
config_entry = FIXTURE_CONFIG_ENTRY.copy()
config_entry["options"] = options
mock_config_entry = MockConfigEntry(**config_entry)
@@ -56,7 +55,7 @@ async def test_migrate_options(
async def test_migrate_options_from_data(hass: HomeAssistant) -> None:
"""Test successful migration of options."""
config_entry = deepcopy(FIXTURE_CONFIG_ENTRY)
config_entry = FIXTURE_CONFIG_ENTRY.copy()
config_entry["options"] = {}
config_entry["data"].update({CONF_READ_ONLY: False})
@@ -108,8 +107,7 @@ async def test_migrate_unique_ids(
entity_registry: er.EntityRegistry,
) -> None:
"""Test successful migration of entity unique_ids."""
confg_entry = deepcopy(FIXTURE_CONFIG_ENTRY)
mock_config_entry = MockConfigEntry(**confg_entry)
mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY)
mock_config_entry.add_to_hass(hass)
entity: er.RegistryEntry = entity_registry.async_get_or_create(
@@ -155,8 +153,7 @@ async def test_dont_migrate_unique_ids(
entity_registry: er.EntityRegistry,
) -> None:
"""Test successful migration of entity unique_ids."""
confg_entry = deepcopy(FIXTURE_CONFIG_ENTRY)
mock_config_entry = MockConfigEntry(**confg_entry)
mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY)
mock_config_entry.add_to_hass(hass)
# create existing entry with new_unique_id
@@ -199,8 +196,7 @@ async def test_remove_stale_devices(
device_registry: dr.DeviceRegistry,
) -> None:
"""Test remove stale device registry entries."""
config_entry = deepcopy(FIXTURE_CONFIG_ENTRY)
mock_config_entry = MockConfigEntry(**config_entry)
mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY)
mock_config_entry.add_to_hass(hass)
device_registry.async_get_or_create(
+1 -1
View File
@@ -151,7 +151,7 @@ class FritzDeviceSwitchMock(FritzEntityBaseMock):
has_thermostat = False
has_blind = False
switch_state = "fake_state"
lock = False
lock = "fake_locked"
power = 5678
present = True
temperature = 1.23
-30
View File
@@ -3,7 +3,6 @@
from datetime import timedelta
from unittest.mock import Mock
import pytest
from requests.exceptions import HTTPError
from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN
@@ -30,7 +29,6 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
import homeassistant.util.dt as dt_util
@@ -132,7 +130,6 @@ async def test_turn_on(hass: HomeAssistant, fritz: Mock) -> None:
async def test_turn_off(hass: HomeAssistant, fritz: Mock) -> None:
"""Test turn device off."""
device = FritzDeviceSwitchMock()
assert await setup_config_entry(
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
)
@@ -140,36 +137,9 @@ async def test_turn_off(hass: HomeAssistant, fritz: Mock) -> None:
await hass.services.async_call(
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True
)
assert device.set_switch_state_off.call_count == 1
async def test_toggle_while_locked(hass: HomeAssistant, fritz: Mock) -> None:
"""Test toggling while device is locked."""
device = FritzDeviceSwitchMock()
device.lock = True
assert await setup_config_entry(
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
)
with pytest.raises(
HomeAssistantError,
match="Can't toggle switch while manual switching is disabled for the device",
):
await hass.services.async_call(
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True
)
with pytest.raises(
HomeAssistantError,
match="Can't toggle switch while manual switching is disabled for the device",
):
await hass.services.async_call(
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True
)
async def test_update(hass: HomeAssistant, fritz: Mock) -> None:
"""Test update without error."""
device = FritzDeviceSwitchMock()
+1 -1
View File
@@ -543,5 +543,5 @@ async def test_register_static_paths(
"Detected code that calls hass.http.register_static_path "
"which is deprecated because it does blocking I/O in the "
"event loop, instead call "
"`await hass.http.async_register_static_paths"
"`await hass.http.async_register_static_path"
) in caplog.text
+1 -1
View File
@@ -1561,7 +1561,7 @@ async def test_setup_with_advanced_settings(
}
@pytest.mark.usefixtures("mock_ssl_context", "mock_process_uploaded_file")
@pytest.mark.usesfixtures("mock_ssl_context", "mock_process_uploaded_file")
async def test_change_websockets_transport_to_tcp(
hass: HomeAssistant, mock_try_connection: MagicMock
) -> None:
+1 -1
View File
@@ -499,7 +499,7 @@ async def test_image_from_url_fails(
),
],
)
@pytest.mark.usefixtures("hass", "hass_client_no_auth")
@pytest.mark.usesfixtures("hass", "hass_client_no_auth")
async def test_image_config_fails(
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
-1
View File
@@ -29,7 +29,6 @@ async def test_unload_entry(
await hass.async_block_till_done()
assert mock_disconnect.call_count == 2
assert setup_entry.state is ConfigEntryState.NOT_LOADED
assert not hass.data.get(DOMAIN)
async def test_config_entry_not_ready(
@@ -7,7 +7,7 @@ from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.shelly.const import UPDATE_PERIOD_MULTIPLIER
from homeassistant.components.shelly.const import SLEEP_PERIOD_MULTIPLIER
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.device_registry import DeviceRegistry
@@ -122,7 +122,7 @@ async def test_block_rest_binary_sensor_connected_battery_devices(
assert hass.states.get(entity_id).state == STATE_OFF
# Verify update on slow intervals
await mock_rest_update(hass, freezer, seconds=UPDATE_PERIOD_MULTIPLIER * 3600)
await mock_rest_update(hass, freezer, seconds=SLEEP_PERIOD_MULTIPLIER * 3600)
assert hass.states.get(entity_id).state == STATE_ON
entry = entity_registry.async_get(entity_id)
+5 -4
View File
@@ -20,6 +20,7 @@ from homeassistant.components.shelly.const import (
ENTRY_RELOAD_COOLDOWN,
MAX_PUSH_UPDATE_FAILURES,
RPC_RECONNECT_INTERVAL,
SLEEP_PERIOD_MULTIPLIER,
UPDATE_PERIOD_MULTIPLIER,
BLEScannerMode,
)
@@ -563,7 +564,7 @@ async def test_rpc_update_entry_sleep_period(
# Move time to generate sleep period update
monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 3600)
freezer.tick(timedelta(seconds=600 * UPDATE_PERIOD_MULTIPLIER))
freezer.tick(timedelta(seconds=600 * SLEEP_PERIOD_MULTIPLIER))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
@@ -595,7 +596,7 @@ async def test_rpc_sleeping_device_no_periodic_updates(
assert get_entity_state(hass, entity_id) == "22.9"
# Move time to generate polling
freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 1000))
freezer.tick(timedelta(seconds=SLEEP_PERIOD_MULTIPLIER * 1000))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
@@ -888,7 +889,7 @@ async def test_block_sleeping_device_connection_error(
assert get_entity_state(hass, entity_id) == STATE_ON
# Move time to generate sleep period update
freezer.tick(timedelta(seconds=sleep_period * UPDATE_PERIOD_MULTIPLIER))
freezer.tick(timedelta(seconds=sleep_period * SLEEP_PERIOD_MULTIPLIER))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
@@ -933,7 +934,7 @@ async def test_rpc_sleeping_device_connection_error(
assert get_entity_state(hass, entity_id) == STATE_ON
# Move time to generate sleep period update
freezer.tick(timedelta(seconds=sleep_period * UPDATE_PERIOD_MULTIPLIER))
freezer.tick(timedelta(seconds=sleep_period * SLEEP_PERIOD_MULTIPLIER))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
+1 -12
View File
@@ -1,10 +1,9 @@
"""Tests for Shelly diagnostics platform."""
from unittest.mock import ANY, Mock, PropertyMock
from unittest.mock import ANY, Mock
from aioshelly.ble.const import BLE_SCAN_RESULT_EVENT
from aioshelly.const import MODEL_25
from aioshelly.exceptions import DeviceConnectionError
import pytest
from homeassistant.components.diagnostics import REDACTED
@@ -37,10 +36,6 @@ async def test_block_config_entry_diagnostics(
{key: REDACTED for key in TO_REDACT if key in entry_dict["data"]}
)
type(mock_block_device).last_error = PropertyMock(
return_value=DeviceConnectionError()
)
result = await get_diagnostics_for_config_entry(hass, hass_client, entry)
assert result == {
@@ -53,7 +48,6 @@ async def test_block_config_entry_diagnostics(
},
"device_settings": {"coiot": {"update_period": 15}},
"device_status": MOCK_STATUS_COAP,
"last_error": "DeviceConnectionError()",
}
@@ -97,10 +91,6 @@ async def test_rpc_config_entry_diagnostics(
{key: REDACTED for key in TO_REDACT if key in entry_dict["data"]}
)
type(mock_rpc_device).last_error = PropertyMock(
return_value=DeviceConnectionError()
)
result = await get_diagnostics_for_config_entry(hass, hass_client, entry)
assert result == {
@@ -162,5 +152,4 @@ async def test_rpc_config_entry_diagnostics(
},
"wifi": {"rssi": -63},
},
"last_error": "DeviceConnectionError()",
}
+36
View File
@@ -3052,3 +3052,39 @@ async def test_primary_config_entry(
model="model",
)
assert device.primary_config_entry == mock_config_entry_1.entry_id
@pytest.mark.parametrize("disabled_by", list(dr.DeviceEntryDisabler))
async def test_add_config_entry_to_disabled_device(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
disabled_by: dr.DeviceEntryDisabler,
) -> None:
"""Test adding config entry to a disabled device."""
mock_config_entry_1 = MockConfigEntry(title=None)
mock_config_entry_1.add_to_hass(hass)
mock_config_entry_2 = MockConfigEntry(
disabled_by=config_entries.ConfigEntryDisabler.USER, title=None
)
mock_config_entry_2.add_to_hass(hass)
mock_config_entry_3 = MockConfigEntry(title=None)
mock_config_entry_3.add_to_hass(hass)
device = device_registry.async_get_or_create(
config_entry_id=mock_config_entry_1.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
disabled_by=disabled_by,
)
assert device.disabled_by == disabled_by
device = device_registry.async_update_device(
device.id, add_config_entry_id=mock_config_entry_2.entry_id
)
assert device
assert device.disabled_by == disabled_by
device = device_registry.async_update_device(
device.id, add_config_entry_id=mock_config_entry_3.entry_id
)
assert device
assert device.disabled_by is None