diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 637066629db..88b918f699c 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone", "iot_class": "local_polling", "loggers": ["aioairzone"], - "requirements": ["aioairzone==0.6.3"] + "requirements": ["aioairzone==0.6.4"] } diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 659243df733..f4fc6a8df08 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -173,7 +173,11 @@ async def async_setup_scanner( rssi = await hass.async_add_executor_job(client.request_rssi) client.close() - tasks.append(see_device(hass, async_see, mac, friendly_name, rssi)) + tasks.append( + asyncio.create_task( + see_device(hass, async_see, mac, friendly_name, rssi) + ) + ) if tasks: await asyncio.wait(tasks) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index c9612d00c64..d30198bdc12 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], - "requirements": ["bimmer_connected==0.13.6"] + "requirements": ["bimmer-connected==0.13.7"] } diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index ef3d9bc002d..91f4940a4e5 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -20,5 +20,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/bthome", "iot_class": "local_push", - "requirements": ["bthome-ble==2.11.3"] + "requirements": ["bthome-ble==2.12.0"] } diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index f8693c5fb34..fc8673e801b 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -47,6 +47,15 @@ from .coordinator import ( from .device import device_key_to_bluetooth_entity_key SENSOR_DESCRIPTIONS = { + # Acceleration (m/s²) + ( + BTHomeSensorDeviceClass.ACCELERATION, + Units.ACCELERATION_METERS_PER_SQUARE_SECOND, + ): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.ACCELERATION}_{Units.ACCELERATION_METERS_PER_SQUARE_SECOND}", + native_unit_of_measurement=Units.ACCELERATION_METERS_PER_SQUARE_SECOND, + state_class=SensorStateClass.MEASUREMENT, + ), # Battery (percent) (BTHomeSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.BATTERY}_{Units.PERCENTAGE}", @@ -131,6 +140,15 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, state_class=SensorStateClass.TOTAL, ), + # Gyroscope (°/s) + ( + BTHomeSensorDeviceClass.GYROSCOPE, + Units.GYROSCOPE_DEGREES_PER_SECOND, + ): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.GYROSCOPE}_{Units.GYROSCOPE_DEGREES_PER_SECOND}", + native_unit_of_measurement=Units.GYROSCOPE_DEGREES_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, + ), # Humidity in (percent) (BTHomeSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}", @@ -242,6 +260,15 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), + # Timestamp (datetime object) + ( + BTHomeSensorDeviceClass.TIMESTAMP, + None, + ): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.TIMESTAMP}", + device_class=SensorDeviceClass.TIMESTAMP, + state_class=SensorStateClass.MEASUREMENT, + ), # UV index (-) ( BTHomeSensorDeviceClass.UV_INDEX, diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py index 95a418ae40f..d941375c8a3 100644 --- a/homeassistant/components/fortios/device_tracker.py +++ b/homeassistant/components/fortios/device_tracker.py @@ -43,7 +43,7 @@ def get_scanner(hass: HomeAssistant, config: ConfigType) -> FortiOSDeviceScanner fgt = FortiOSAPI() try: - fgt.tokenlogin(host, token, verify_ssl) + fgt.tokenlogin(host, token, verify_ssl, None, 12, "root") except ConnectionError as ex: _LOGGER.error("ConnectionError to FortiOS API: %s", ex) return None @@ -77,7 +77,12 @@ class FortiOSDeviceScanner(DeviceScanner): def update(self): """Update clients from the device.""" - clients_json = self._fgt.monitor("user/device/query", "") + clients_json = self._fgt.monitor( + "user/device/query", + "", + parameters={"filter": "format=master_mac|hostname|is_online"}, + ) + self._clients_json = clients_json self._clients = [] @@ -85,8 +90,12 @@ class FortiOSDeviceScanner(DeviceScanner): if clients_json: try: for client in clients_json["results"]: - if client["is_online"]: - self._clients.append(client["mac"].upper()) + if ( + "is_online" in client + and "master_mac" in client + and client["is_online"] + ): + self._clients.append(client["master_mac"].upper()) except KeyError as kex: _LOGGER.error("Key not found in clients: %s", kex) @@ -106,17 +115,10 @@ class FortiOSDeviceScanner(DeviceScanner): return None for client in data["results"]: - if client["mac"] == device: - try: + if "master_mac" in client and client["master_mac"] == device: + if "hostname" in client: name = client["hostname"] - _LOGGER.debug("Getting device name=%s", name) - return name - except KeyError as kex: - _LOGGER.debug( - "No hostname found for %s in client data: %s", - device, - kex, - ) - return device.replace(":", "_") - + else: + name = client["master_mac"].replace(":", "_") + return name return None diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index dd1cc70c9f4..217e73e4d1c 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -2,6 +2,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN from .coordinator import FullyKioskDataUpdateCoordinator @@ -17,6 +18,14 @@ PLATFORMS = [ ] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Fully Kiosk Browser.""" + + await async_setup_services(hass) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fully Kiosk Browser from a config entry.""" @@ -28,8 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) coordinator.async_update_listeners() - await async_setup_services(hass) - return True diff --git a/homeassistant/components/fully_kiosk/services.py b/homeassistant/components/fully_kiosk/services.py index 3fca9228735..b3c5886187a 100644 --- a/homeassistant/components/fully_kiosk/services.py +++ b/homeassistant/components/fully_kiosk/services.py @@ -1,14 +1,12 @@ """Services for the Fully Kiosk Browser integration.""" from __future__ import annotations -from collections.abc import Callable -from typing import Any - -from fullykiosk import FullyKiosk import voluptuous as vol +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr @@ -16,59 +14,53 @@ from .const import ( ATTR_APPLICATION, ATTR_URL, DOMAIN, - LOGGER, SERVICE_LOAD_URL, SERVICE_START_APPLICATION, ) +from .coordinator import FullyKioskDataUpdateCoordinator async def async_setup_services(hass: HomeAssistant) -> None: """Set up the services for the Fully Kiosk Browser integration.""" - async def execute_service( - call: ServiceCall, - fully_method: Callable, - *args: list[str], - **kwargs: dict[str, Any], - ) -> None: - """Execute a Fully service call. - - :param call: {ServiceCall} HA service call. - :param fully_method: {Callable} A method of the FullyKiosk class. - :param args: Arguments for fully_method. - :param kwargs: Key-word arguments for fully_method. - :return: None - """ - LOGGER.debug( - "Calling Fully service %s with args: %s, %s", ServiceCall, args, kwargs - ) + async def collect_coordinators( + device_ids: list[str], + ) -> list[FullyKioskDataUpdateCoordinator]: + config_entries = list[ConfigEntry]() registry = dr.async_get(hass) - for target in call.data[ATTR_DEVICE_ID]: + for target in device_ids: device = registry.async_get(target) if device: - for key in device.config_entries: - entry = hass.config_entries.async_get_entry(key) - if not entry: - continue - if entry.domain != DOMAIN: - continue - coordinator = hass.data[DOMAIN][key] - # fully_method(coordinator.fully, *args, **kwargs) would make - # test_services.py fail. - await getattr(coordinator.fully, fully_method.__name__)( - *args, **kwargs + device_entries = list[ConfigEntry]() + for entry_id in device.config_entries: + entry = hass.config_entries.async_get_entry(entry_id) + if entry and entry.domain == DOMAIN: + device_entries.append(entry) + if not device_entries: + raise HomeAssistantError( + f"Device '{target}' is not a {DOMAIN} device" ) - break + config_entries.extend(device_entries) + else: + raise HomeAssistantError( + f"Device '{target}' not found in device registry" + ) + coordinators = list[FullyKioskDataUpdateCoordinator]() + for config_entry in config_entries: + if config_entry.state != ConfigEntryState.LOADED: + raise HomeAssistantError(f"{config_entry.title} is not loaded") + coordinators.append(hass.data[DOMAIN][config_entry.entry_id]) + return coordinators async def async_load_url(call: ServiceCall) -> None: """Load a URL on the Fully Kiosk Browser.""" - await execute_service(call, FullyKiosk.loadUrl, call.data[ATTR_URL]) + for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]): + await coordinator.fully.loadUrl(call.data[ATTR_URL]) async def async_start_app(call: ServiceCall) -> None: """Start an app on the device.""" - await execute_service( - call, FullyKiosk.startApplication, call.data[ATTR_APPLICATION] - ) + for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]): + await coordinator.fully.startApplication(call.data[ATTR_APPLICATION]) # Register all the above services service_mapping = [ diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index f4a3f882749..e952164792f 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -223,13 +223,6 @@ SENSOR_TYPES = { icon="mdi:docker", state_class=SensorStateClass.MEASUREMENT, ), - ("raid", "used"): GlancesSensorEntityDescription( - key="used", - type="raid", - name_suffix="Raid used", - icon="mdi:harddisk", - state_class=SensorStateClass.MEASUREMENT, - ), ("raid", "available"): GlancesSensorEntityDescription( key="available", type="raid", @@ -237,6 +230,13 @@ SENSOR_TYPES = { icon="mdi:harddisk", state_class=SensorStateClass.MEASUREMENT, ), + ("raid", "used"): GlancesSensorEntityDescription( + key="used", + type="raid", + name_suffix="Raid used", + icon="mdi:harddisk", + state_class=SensorStateClass.MEASUREMENT, + ), } @@ -269,36 +269,36 @@ async def async_setup_entry( if sensor_type in ["fs", "sensors", "raid"]: for sensor_label, params in sensors.items(): for param in params: - sensor_description = SENSOR_TYPES[(sensor_type, param)] + if sensor_description := SENSOR_TYPES.get((sensor_type, param)): + _migrate_old_unique_ids( + hass, + f"{coordinator.host}-{name} {sensor_label} {sensor_description.name_suffix}", + f"{sensor_label}-{sensor_description.key}", + ) + entities.append( + GlancesSensor( + coordinator, + name, + sensor_label, + sensor_description, + ) + ) + else: + for sensor in sensors: + if sensor_description := SENSOR_TYPES.get((sensor_type, sensor)): _migrate_old_unique_ids( hass, - f"{coordinator.host}-{name} {sensor_label} {sensor_description.name_suffix}", - f"{sensor_label}-{sensor_description.key}", + f"{coordinator.host}-{name} {sensor_description.name_suffix}", + f"-{sensor_description.key}", ) entities.append( GlancesSensor( coordinator, name, - sensor_label, + "", sensor_description, ) ) - else: - for sensor in sensors: - sensor_description = SENSOR_TYPES[(sensor_type, sensor)] - _migrate_old_unique_ids( - hass, - f"{coordinator.host}-{name} {sensor_description.name_suffix}", - f"-{sensor_description.key}", - ) - entities.append( - GlancesSensor( - coordinator, - name, - "", - sensor_description, - ) - ) async_add_entities(entities) diff --git a/homeassistant/components/goodwe/sensor.py b/homeassistant/components/goodwe/sensor.py index d76d6202832..4a4296bc526 100644 --- a/homeassistant/components/goodwe/sensor.py +++ b/homeassistant/components/goodwe/sensor.py @@ -243,7 +243,7 @@ class InverterSensor(CoordinatorEntity[GoodweUpdateCoordinator], SensorEntity): In contrast to "total" sensors, these "daily" sensors need to be reset to 0 on midnight. """ if not self.coordinator.last_update_success: - self.coordinator.reset_sensor(self._sensor.id) + self.coordinator.reset_sensor(self._sensor.id_) self.async_write_ha_state() _LOGGER.debug("Goodwe reset %s to 0", self.name) next_midnight = dt_util.start_of_local_day( diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index e085167301f..d9e0fb227c0 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -24,7 +24,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"]) SENSORS_TYPES = { - "name": SensorType("Name", None, "", ["profile", "name"]), + "name": SensorType("Name", None, None, ["profile", "name"]), "hp": SensorType("HP", "mdi:heart", "HP", ["stats", "hp"]), "maxHealth": SensorType("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]), "mp": SensorType("Mana", "mdi:auto-fix", "MP", ["stats", "mp"]), @@ -35,7 +35,7 @@ SENSORS_TYPES = { "Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"] ), "gp": SensorType("Gold", "mdi:circle-multiple", "Gold", ["stats", "gp"]), - "class": SensorType("Class", "mdi:sword", "", ["stats", "class"]), + "class": SensorType("Class", "mdi:sword", None, ["stats", "class"]), } TASKS_TYPES = { diff --git a/homeassistant/components/hassio/issues.py b/homeassistant/components/hassio/issues.py index a92fc392fa4..8f7f06a3931 100644 --- a/homeassistant/components/hassio/issues.py +++ b/homeassistant/components/hassio/issues.py @@ -305,7 +305,11 @@ class SupervisorIssues: async def update(self) -> None: """Update issues from Supervisor resolution center.""" - data = await self._client.get_resolution_info() + try: + data = await self._client.get_resolution_info() + except HassioAPIError as err: + _LOGGER.error("Failed to update supervisor issues: %r", err) + return self.unhealthy_reasons = set(data[ATTR_UNHEALTHY]) self.unsupported_reasons = set(data[ATTR_UNSUPPORTED]) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index dc8a2a7c639..a2e3f8487c6 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -626,10 +626,10 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc] @pyhap_callback # type: ignore[misc] def pair( - self, client_uuid: UUID, client_public: str, client_permissions: int + self, client_username_bytes: bytes, client_public: str, client_permissions: int ) -> bool: """Override super function to dismiss setup message if paired.""" - success = super().pair(client_uuid, client_public, client_permissions) + success = super().pair(client_username_bytes, client_public, client_permissions) if success: async_dismiss_setup_message(self.hass, self._entry_id) return cast(bool, success) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 746b097e99a..245dbd0a19e 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["pyhap"], "requirements": [ - "HAP-python==4.6.0", + "HAP-python==4.7.0", "fnv-hash-fast==0.3.1", "PyQRCode==1.2.1", "base36==0.1.1" diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index f9c22ef62a5..d7cbe676eee 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -115,8 +115,8 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None: """Register Insteon device events.""" @callback - def async_fire_group_on_off_event( - name: str, address: Address, group: int, button: str + def async_fire_insteon_event( + name: str, address: Address, group: int, button: str | None = None ): # Firing an event when a button is pressed. if button and button[-2] == "_": @@ -146,9 +146,9 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None: for name_or_group, event in device.events.items(): if isinstance(name_or_group, int): for _, event in device.events[name_or_group].items(): - _register_event(event, async_fire_group_on_off_event) + _register_event(event, async_fire_insteon_event) else: - _register_event(event, async_fire_group_on_off_event) + _register_event(event, async_fire_insteon_event) def register_new_device_callback(hass): diff --git a/homeassistant/components/knx/device_trigger.py b/homeassistant/components/knx/device_trigger.py index 8a074b43b7d..1abafb221db 100644 --- a/homeassistant/components/knx/device_trigger.py +++ b/homeassistant/components/knx/device_trigger.py @@ -84,6 +84,7 @@ async def async_attach_trigger( trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" + trigger_data = trigger_info["trigger_data"] dst_addresses: list[str] = config.get(EXTRA_FIELD_DESTINATION, []) job = HassJob(action, f"KNX device trigger {trigger_info}") knx: KNXModule = hass.data[DOMAIN] @@ -95,7 +96,7 @@ async def async_attach_trigger( return hass.async_run_hass_job( job, - {"trigger": telegram}, + {"trigger": {**trigger_data, **telegram}}, ) return knx.telegrams.async_listen_telegram( diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index d8cf96be5ac..08179df5b7e 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import hashlib -from pylast import LastFMNetwork, Track, User, WSError +from pylast import LastFMNetwork, PyLastError, Track, User import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -104,26 +104,30 @@ class LastFmSensor(SensorEntity): def update(self) -> None: """Update device state.""" + self._attr_native_value = STATE_NOT_SCROBBLING try: - self._user.get_playcount() - except WSError as exc: + play_count = self._user.get_playcount() + self._attr_entity_picture = self._user.get_image() + now_playing = self._user.get_now_playing() + top_tracks = self._user.get_top_tracks(limit=1) + last_tracks = self._user.get_recent_tracks(limit=1) + except PyLastError as exc: self._attr_available = False LOGGER.error("Failed to load LastFM user `%s`: %r", self._user.name, exc) return - self._attr_entity_picture = self._user.get_image() - if now_playing := self._user.get_now_playing(): + self._attr_available = True + if now_playing: self._attr_native_value = format_track(now_playing) - else: - self._attr_native_value = STATE_NOT_SCROBBLING - top_played = None - if top_tracks := self._user.get_top_tracks(limit=1): - top_played = format_track(top_tracks[0].item) - last_played = None - if last_tracks := self._user.get_recent_tracks(limit=1): - last_played = format_track(last_tracks[0].track) - play_count = self._user.get_playcount() self._attr_extra_state_attributes = { - ATTR_LAST_PLAYED: last_played, ATTR_PLAY_COUNT: play_count, - ATTR_TOP_PLAYED: top_played, + ATTR_LAST_PLAYED: None, + ATTR_TOP_PLAYED: None, } + if len(last_tracks) > 0: + self._attr_extra_state_attributes[ATTR_LAST_PLAYED] = format_track( + last_tracks[0].track + ) + if len(top_tracks) > 0: + self._attr_extra_state_attributes[ATTR_TOP_PLAYED] = format_track( + top_tracks[0].item + ) diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 049f9de03ea..b56acffe4e2 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==4.5.1"] + "requirements": ["ical==4.5.4"] } diff --git a/homeassistant/components/matter/__init__.py b/homeassistant/components/matter/__init__.py index 4c47cd4d235..59c5ec9efc8 100644 --- a/homeassistant/components/matter/__init__.py +++ b/homeassistant/components/matter/__init__.py @@ -2,11 +2,12 @@ from __future__ import annotations import asyncio +from contextlib import suppress import async_timeout from matter_server.client import MatterClient from matter_server.client.exceptions import CannotConnect, InvalidServerVersion -from matter_server.common.errors import MatterError, NodeCommissionFailed +from matter_server.common.errors import MatterError, NodeCommissionFailed, NodeNotExists import voluptuous as vol from homeassistant.components.hassio import AddonError, AddonManager, AddonState @@ -207,7 +208,9 @@ async def async_remove_config_entry_device( ) matter = get_matter(hass) - await matter.matter_client.remove_node(node.node_id) + with suppress(NodeNotExists): + # ignore if the server has already removed the node. + await matter.matter_client.remove_node(node.node_id) return True diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index 7d73ceafc7a..9f16dae8334 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -92,7 +92,7 @@ class MatterAdapter: get_clean_name(basic_info.nodeLabel) or get_clean_name(basic_info.productLabel) or get_clean_name(basic_info.productName) - or device_type.__class__.__name__ + or device_type.__name__ if device_type else None ) @@ -117,7 +117,7 @@ class MatterAdapter: identifiers.add((DOMAIN, f"{ID_TYPE_SERIAL}_{basic_info.serialNumber}")) model = ( - get_clean_name(basic_info.productName) or device_type.__class__.__name__ + get_clean_name(basic_info.productName) or device_type.__name__ if device_type else None ) diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 5af01f2eea5..707f7e70ee3 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", - "requirements": ["python-matter-server==3.4.1"] + "requirements": ["python-matter-server==3.5.1"] } diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 3b82399f217..ccd23762850 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -133,10 +133,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator_alert.async_refresh() - if not coordinator_alert.last_update_success: - raise ConfigEntryNotReady - - hass.data[DOMAIN][department] = True + if coordinator_alert.last_update_success: + hass.data[DOMAIN][department] = True else: _LOGGER.warning( ( @@ -158,11 +156,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: undo_listener = entry.add_update_listener(_async_update_listener) hass.data[DOMAIN][entry.entry_id] = { + UNDO_UPDATE_LISTENER: undo_listener, COORDINATOR_FORECAST: coordinator_forecast, COORDINATOR_RAIN: coordinator_rain, - COORDINATOR_ALERT: coordinator_alert, - UNDO_UPDATE_LISTENER: undo_listener, } + if coordinator_alert and coordinator_alert.last_update_success: + hass.data[DOMAIN][entry.entry_id][COORDINATOR_ALERT] = coordinator_alert await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/rapt_ble/manifest.json b/homeassistant/components/rapt_ble/manifest.json index d3eab0641a6..1bde135de35 100644 --- a/homeassistant/components/rapt_ble/manifest.json +++ b/homeassistant/components/rapt_ble/manifest.json @@ -16,5 +16,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/rapt_ble", "iot_class": "local_push", - "requirements": ["rapt-ble==0.1.1"] + "requirements": ["rapt-ble==0.1.2"] } diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py index ef252d54e4e..9caa642b5f4 100644 --- a/homeassistant/components/totalconnect/binary_sensor.py +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -81,15 +81,9 @@ class TotalConnectZoneSecurityBinarySensor(TotalConnectZoneBinarySensor): return BinarySensorDeviceClass.MOTION if self._zone.is_type_medical(): return BinarySensorDeviceClass.SAFETY - # "security" type is a generic category so test for it last - if self._zone.is_type_security(): - return BinarySensorDeviceClass.DOOR - - _LOGGER.error( - "TotalConnect zone %s reported an unexpected device class", - self._zone.zoneid, - ) - return None + if self._zone.is_type_temperature(): + return BinarySensorDeviceClass.PROBLEM + return BinarySensorDeviceClass.DOOR def update(self): """Return the state of the device.""" diff --git a/homeassistant/components/youtube/coordinator.py b/homeassistant/components/youtube/coordinator.py index 190e79e3393..d3430d8329c 100644 --- a/homeassistant/components/youtube/coordinator.py +++ b/homeassistant/components/youtube/coordinator.py @@ -85,9 +85,16 @@ class YouTubeDataUpdateCoordinator(DataUpdateCoordinator): ATTR_PUBLISHED_AT: video["snippet"]["publishedAt"], ATTR_TITLE: video["snippet"]["title"], ATTR_DESCRIPTION: video["snippet"]["description"], - ATTR_THUMBNAIL: video["snippet"]["thumbnails"]["standard"]["url"], + ATTR_THUMBNAIL: self._get_thumbnail(video), ATTR_VIDEO_ID: video["contentDetails"]["videoId"], }, ATTR_SUBSCRIBER_COUNT: int(channel["statistics"]["subscriberCount"]), } return data + + def _get_thumbnail(self, video: dict[str, Any]) -> str | None: + thumbnails = video["snippet"]["thumbnails"] + for size in ("standard", "high", "medium", "default"): + if size in thumbnails: + return thumbnails[size]["url"] + return None diff --git a/homeassistant/components/youtube/sensor.py b/homeassistant/components/youtube/sensor.py index c605b960475..4560dcfda8c 100644 --- a/homeassistant/components/youtube/sensor.py +++ b/homeassistant/components/youtube/sensor.py @@ -30,7 +30,7 @@ class YouTubeMixin: """Mixin for required keys.""" value_fn: Callable[[Any], StateType] - entity_picture_fn: Callable[[Any], str] + entity_picture_fn: Callable[[Any], str | None] attributes_fn: Callable[[Any], dict[str, Any]] | None @@ -87,7 +87,7 @@ class YouTubeSensor(YouTubeChannelEntity, SensorEntity): return self.entity_description.value_fn(self.coordinator.data[self._channel_id]) @property - def entity_picture(self) -> str: + def entity_picture(self) -> str | None: """Return the value reported by the sensor.""" return self.entity_description.entity_picture_fn( self.coordinator.data[self._channel_id] diff --git a/homeassistant/components/youtube/strings.json b/homeassistant/components/youtube/strings.json index eb89738708e..1ecc2bc4db8 100644 --- a/homeassistant/components/youtube/strings.json +++ b/homeassistant/components/youtube/strings.json @@ -17,6 +17,10 @@ "data": { "channels": "YouTube channels" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The YouTube integration needs to re-authenticate your account" } } }, diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 4f52c41a085..2fe2b17fe1b 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -65,7 +65,7 @@ def redact_node_state(node_state: NodeDataType) -> NodeDataType: def get_device_entities( - hass: HomeAssistant, node: Node, device: dr.DeviceEntry + hass: HomeAssistant, node: Node, config_entry: ConfigEntry, device: dr.DeviceEntry ) -> list[dict[str, Any]]: """Get entities for a device.""" entity_entries = er.async_entries_for_device( @@ -73,6 +73,10 @@ def get_device_entities( ) entities = [] for entry in entity_entries: + # Skip entities that are not part of this integration + if entry.config_entry_id != config_entry.entry_id: + continue + # If the value ID returns as None, we don't need to include this entity if (value_id := get_value_id_from_unique_id(entry.unique_id)) is None: continue @@ -142,7 +146,7 @@ async def async_get_device_diagnostics( if node_id is None or node_id not in driver.controller.nodes: raise ValueError(f"Node for device {device.id} can't be found") node = driver.controller.nodes[node_id] - entities = get_device_entities(hass, node, device) + entities = get_device_entities(hass, node, config_entry, device) assert client.version node_state = redact_node_state(async_redact_data(node.data, KEYS_TO_REDACT)) return { diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index 32bd3130e03..33cb59d8505 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -142,8 +142,9 @@ async def async_attach_trigger( ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" dev_reg = dr.async_get(hass) - nodes = async_get_nodes_from_targets(hass, config, dev_reg=dev_reg) - if config[ATTR_EVENT_SOURCE] == "node" and not nodes: + if config[ATTR_EVENT_SOURCE] == "node" and not async_get_nodes_from_targets( + hass, config, dev_reg=dev_reg + ): raise ValueError( f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." ) @@ -215,7 +216,7 @@ async def async_attach_trigger( # Nodes list can come from different drivers and we will need to listen to # server connections for all of them. drivers: set[Driver] = set() - if not nodes: + if not (nodes := async_get_nodes_from_targets(hass, config, dev_reg=dev_reg)): entry_id = config[ATTR_CONFIG_ENTRY_ID] client: Client = hass.data[DOMAIN][entry_id][DATA_CLIENT] driver = client.driver diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index 4e21774c98f..52ecc0a7742 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -91,7 +91,7 @@ async def async_attach_trigger( ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" dev_reg = dr.async_get(hass) - if not (nodes := async_get_nodes_from_targets(hass, config, dev_reg=dev_reg)): + if not async_get_nodes_from_targets(hass, config, dev_reg=dev_reg): raise ValueError( f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." ) @@ -174,7 +174,7 @@ async def async_attach_trigger( # Nodes list can come from different drivers and we will need to listen to # server connections for all of them. drivers: set[Driver] = set() - for node in nodes: + for node in async_get_nodes_from_targets(hass, config, dev_reg=dev_reg): driver = node.client.driver assert driver is not None # The node comes from the driver. drivers.add(driver) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2d5e5ebb14c..71edc76d9dc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a7688415905..3c4fc9a6746 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -46,7 +46,7 @@ pyyaml==6.0 requests==2.31.0 scapy==2.5.0 sqlalchemy==2.0.15 -typing-extensions>=4.5.0,<5.0 +typing_extensions>=4.6.3,<5.0 ulid-transform==0.7.2 voluptuous-serialize==2.6.0 voluptuous==0.13.1 diff --git a/pyproject.toml b/pyproject.toml index ff8898aa90a..563b07570a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.6.2" +version = "2023.6.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" @@ -49,7 +49,7 @@ dependencies = [ "python-slugify==4.0.1", "pyyaml==6.0", "requests==2.31.0", - "typing-extensions>=4.5.0,<5.0", + "typing_extensions>=4.6.3,<5.0", "ulid-transform==0.7.2", "voluptuous==0.13.1", "voluptuous-serialize==2.6.0", diff --git a/requirements.txt b/requirements.txt index 818eeec8515..48dfff5d662 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ pip>=21.0,<23.2 python-slugify==4.0.1 pyyaml==6.0 requests==2.31.0 -typing-extensions>=4.5.0,<5.0 +typing_extensions>=4.6.3,<5.0 ulid-transform==0.7.2 voluptuous==0.13.1 voluptuous-serialize==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7e62831c75e..2d3fab79538 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -11,7 +11,7 @@ AIOAladdinConnect==0.1.56 Adax-local==0.1.5 # homeassistant.components.homekit -HAP-python==4.6.0 +HAP-python==4.7.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 @@ -119,7 +119,7 @@ aioairq==0.2.4 aioairzone-cloud==0.1.8 # homeassistant.components.airzone -aioairzone==0.6.3 +aioairzone==0.6.4 # homeassistant.components.ambient_station aioambient==2023.04.0 @@ -434,7 +434,7 @@ beautifulsoup4==4.11.1 bellows==0.35.5 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.13.6 +bimmer-connected==0.13.7 # homeassistant.components.bizkaibus bizkaibus==0.1.1 @@ -502,7 +502,7 @@ brunt==1.2.0 bt_proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==2.11.3 +bthome-ble==2.12.0 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 @@ -966,7 +966,7 @@ ibeacon_ble==1.0.1 ibmiotf==0.3.4 # homeassistant.components.local_calendar -ical==4.5.1 +ical==4.5.4 # homeassistant.components.ping icmplib==3.0 @@ -2087,7 +2087,7 @@ python-kasa==0.5.1 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==3.4.1 +python-matter-server==3.5.1 # homeassistant.components.xiaomi_miio python-miio==0.5.12 @@ -2240,7 +2240,7 @@ radiotherm==2.1.0 raincloudy==0.0.7 # homeassistant.components.rapt_ble -rapt-ble==0.1.1 +rapt-ble==0.1.2 # homeassistant.components.raspyrfm raspyrfm-client==1.2.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb5fa31f9fa..712b10c12a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -13,7 +13,7 @@ AIOAladdinConnect==0.1.56 Adax-local==0.1.5 # homeassistant.components.homekit -HAP-python==4.6.0 +HAP-python==4.7.0 # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -109,7 +109,7 @@ aioairq==0.2.4 aioairzone-cloud==0.1.8 # homeassistant.components.airzone -aioairzone==0.6.3 +aioairzone==0.6.4 # homeassistant.components.ambient_station aioambient==2023.04.0 @@ -367,7 +367,7 @@ beautifulsoup4==4.11.1 bellows==0.35.5 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.13.6 +bimmer-connected==0.13.7 # homeassistant.components.bluetooth bleak-retry-connector==3.0.2 @@ -415,7 +415,7 @@ brottsplatskartan==0.0.1 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==2.11.3 +bthome-ble==2.12.0 # homeassistant.components.buienradar buienradar==1.0.5 @@ -746,7 +746,7 @@ iaqualink==0.5.0 ibeacon_ble==1.0.1 # homeassistant.components.local_calendar -ical==4.5.1 +ical==4.5.4 # homeassistant.components.ping icmplib==3.0 @@ -1522,7 +1522,7 @@ python-juicenet==1.1.0 python-kasa==0.5.1 # homeassistant.components.matter -python-matter-server==3.4.1 +python-matter-server==3.5.1 # homeassistant.components.xiaomi_miio python-miio==0.5.12 @@ -1627,7 +1627,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rapt_ble -rapt-ble==0.1.1 +rapt-ble==0.1.2 # homeassistant.components.rainmachine regenmaschine==2023.06.0 diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index 7aafe7ba7a9..4450bfcc936 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -858,6 +858,57 @@ async def test_v1_sensors( }, ], ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x44\x50\x5D\x39\x61\x64", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_timestamp", + "friendly_name": "Test Device 18B2 Timestamp", + "unit_of_measurement": "s", + "state_class": "measurement", + "expected_state": "2023-05-14T19:41:17+00:00", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x44\x51\x87\x56", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_acceleration", + "friendly_name": "Test Device 18B2 Acceleration", + "unit_of_measurement": "m/s²", + "state_class": "measurement", + "expected_state": "22.151", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x44\x52\x87\x56", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_gyroscope", + "friendly_name": "Test Device 18B2 Gyroscope", + "unit_of_measurement": "°/s", + "state_class": "measurement", + "expected_state": "22.151", + }, + ], + ), ( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( diff --git a/tests/components/fully_kiosk/test_services.py b/tests/components/fully_kiosk/test_services.py index 386bc542e3c..504aa4893e6 100644 --- a/tests/components/fully_kiosk/test_services.py +++ b/tests/components/fully_kiosk/test_services.py @@ -1,6 +1,8 @@ """Test Fully Kiosk Browser services.""" from unittest.mock import MagicMock +import pytest + from homeassistant.components.fully_kiosk.const import ( ATTR_APPLICATION, ATTR_URL, @@ -10,6 +12,7 @@ from homeassistant.components.fully_kiosk.const import ( ) from homeassistant.const import ATTR_DEVICE_ID from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from tests.common import MockConfigEntry @@ -28,20 +31,111 @@ async def test_services( assert device_entry + url = "https://example.com" await hass.services.async_call( DOMAIN, SERVICE_LOAD_URL, - {ATTR_DEVICE_ID: [device_entry.id], ATTR_URL: "https://example.com"}, + {ATTR_DEVICE_ID: [device_entry.id], ATTR_URL: url}, blocking=True, ) - assert len(mock_fully_kiosk.loadUrl.mock_calls) == 1 + mock_fully_kiosk.loadUrl.assert_called_once_with(url) + app = "de.ozerov.fully" await hass.services.async_call( DOMAIN, SERVICE_START_APPLICATION, - {ATTR_DEVICE_ID: [device_entry.id], ATTR_APPLICATION: "de.ozerov.fully"}, + {ATTR_DEVICE_ID: [device_entry.id], ATTR_APPLICATION: app}, blocking=True, ) - assert len(mock_fully_kiosk.startApplication.mock_calls) == 1 + mock_fully_kiosk.startApplication.assert_called_once_with(app) + + +async def test_service_unloaded_entry( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test service not called when config entry unloaded.""" + await init_integration.async_unload(hass) + + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, "abcdef-123456")} + ) + + assert device_entry + + with pytest.raises(HomeAssistantError) as excinfo: + await hass.services.async_call( + DOMAIN, + SERVICE_LOAD_URL, + {ATTR_DEVICE_ID: [device_entry.id], ATTR_URL: "https://nabucasa.com"}, + blocking=True, + ) + assert "Test device is not loaded" in str(excinfo) + mock_fully_kiosk.loadUrl.assert_not_called() + + with pytest.raises(HomeAssistantError) as excinfo: + await hass.services.async_call( + DOMAIN, + SERVICE_START_APPLICATION, + {ATTR_DEVICE_ID: [device_entry.id], ATTR_APPLICATION: "de.ozerov.fully"}, + blocking=True, + ) + assert "Test device is not loaded" in str(excinfo) + mock_fully_kiosk.startApplication.assert_not_called() + + +async def test_service_bad_device_id( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test Fully Kiosk Browser service invocation with bad device id.""" + with pytest.raises(HomeAssistantError) as excinfo: + await hass.services.async_call( + DOMAIN, + SERVICE_LOAD_URL, + {ATTR_DEVICE_ID: ["bad-device_id"], ATTR_URL: "https://example.com"}, + blocking=True, + ) + + assert "Device 'bad-device_id' not found in device registry" in str(excinfo) + + +async def test_service_called_with_non_fkb_target_devices( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Services raise exception when no valid devices provided.""" + device_registry = dr.async_get(hass) + + other_domain = "NotFullyKiosk" + other_config_id = "555" + await hass.config_entries.async_add( + MockConfigEntry( + title="Not Fully Kiosk", domain=other_domain, entry_id=other_config_id + ) + ) + device_entry = device_registry.async_get_or_create( + config_entry_id=other_config_id, + identifiers={ + (other_domain, 1), + }, + ) + + with pytest.raises(HomeAssistantError) as excinfo: + await hass.services.async_call( + DOMAIN, + SERVICE_LOAD_URL, + { + ATTR_DEVICE_ID: [device_entry.id], + ATTR_URL: "https://example.com", + }, + blocking=True, + ) + + assert f"Device '{device_entry.id}' is not a fully_kiosk device" in str(excinfo) diff --git a/tests/components/glances/__init__.py b/tests/components/glances/__init__.py index 064c5ab0eb5..41f2675c41c 100644 --- a/tests/components/glances/__init__.py +++ b/tests/components/glances/__init__.py @@ -137,6 +137,40 @@ MOCK_DATA = { "os_version": "5.15.6-200.fc35.x86_64", "hr_name": "Fedora Linux 35 64bit", }, + "raid": { + "md3": { + "status": "active", + "type": "raid1", + "components": {"sdh1": "2", "sdi1": "0"}, + "available": "2", + "used": "2", + "config": "UU", + }, + "md1": { + "status": "active", + "type": "raid1", + "components": {"sdg": "0", "sde": "1"}, + "available": "2", + "used": "2", + "config": "UU", + }, + "md4": { + "status": "active", + "type": "raid1", + "components": {"sdf1": "1", "sdb1": "0"}, + "available": "2", + "used": "2", + "config": "UU", + }, + "md0": { + "status": "active", + "type": "raid1", + "components": {"sdc": "2", "sdd": "3"}, + "available": "2", + "used": "2", + "config": "UU", + }, + }, "uptime": "3 days, 10:25:20", } @@ -156,4 +190,22 @@ HA_SENSOR_DATA: dict[str, Any] = { "memory_free": 2745.0, }, "docker": {"docker_active": 2, "docker_cpu_use": 77.2, "docker_memory_use": 1149.6}, + "raid": { + "md3": { + "status": "active", + "type": "raid1", + "components": {"sdh1": "2", "sdi1": "0"}, + "available": "2", + "used": "2", + "config": "UU", + }, + "md1": { + "status": "active", + "type": "raid1", + "components": {"sdg": "0", "sde": "1"}, + "available": "2", + "used": "2", + "config": "UU", + }, + }, } diff --git a/tests/components/glances/test_sensor.py b/tests/components/glances/test_sensor.py index 2366e10d11b..d7705854720 100644 --- a/tests/components/glances/test_sensor.py +++ b/tests/components/glances/test_sensor.py @@ -35,6 +35,14 @@ async def test_sensor_states(hass: HomeAssistant) -> None: assert state.state == HA_SENSOR_DATA["docker"]["docker_cpu_use"] if state := hass.states.get("sensor.0_0_0_0_docker_memory_use"): assert state.state == HA_SENSOR_DATA["docker"]["docker_memory_use"] + if state := hass.states.get("sensor.0_0_0_0_md3_available"): + assert state.state == HA_SENSOR_DATA["raid"]["md3"]["available"] + if state := hass.states.get("sensor.0_0_0_0_md3_used"): + assert state.state == HA_SENSOR_DATA["raid"]["md3"]["used"] + if state := hass.states.get("sensor.0_0_0_0_md1_available"): + assert state.state == HA_SENSOR_DATA["raid"]["md1"]["available"] + if state := hass.states.get("sensor.0_0_0_0_md1_used"): + assert state.state == HA_SENSOR_DATA["raid"]["md1"]["used"] @pytest.mark.parametrize( diff --git a/tests/components/hassio/test_issues.py b/tests/components/hassio/test_issues.py index 7bd30e452c0..4d694b79e46 100644 --- a/tests/components/hassio/test_issues.py +++ b/tests/components/hassio/test_issues.py @@ -715,3 +715,21 @@ async def test_supervisor_remove_missing_issue_without_error( msg = await client.receive_json() assert msg["success"] await hass.async_block_till_done() + + +async def test_system_is_not_ready( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + caplog: pytest.LogCaptureFixture, +) -> None: + """Ensure hassio starts despite error.""" + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "", + "message": "System is not ready with state: setup", + }, + ) + + assert await async_setup_component(hass, "hassio", {}) + assert "Failed to update supervisor issues" in caplog.text diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 0b74763c6a7..0ffa7893fbd 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch +from uuid import uuid1 from pyhap.accessory import Accessory from pyhap.const import CATEGORY_CAMERA, CATEGORY_TELEVISION @@ -868,11 +869,11 @@ async def test_homekit_unpair( homekit.driver.aio_stop_event = MagicMock() state = homekit.driver.state - state.add_paired_client("client1", "any", b"1") - state.add_paired_client("client2", "any", b"0") - state.add_paired_client("client3", "any", b"1") - state.add_paired_client("client4", "any", b"0") - state.add_paired_client("client5", "any", b"0") + state.add_paired_client(str(uuid1()).encode("utf-8"), "any", b"1") + state.add_paired_client(str(uuid1()).encode("utf-8"), "any", b"0") + state.add_paired_client(str(uuid1()).encode("utf-8"), "any", b"1") + state.add_paired_client(str(uuid1()).encode("utf-8"), "any", b"0") + state.add_paired_client(str(uuid1()).encode("utf-8"), "any", b"0") formatted_mac = dr.format_mac(state.mac) hk_bridge_dev = device_registry.async_get_device( @@ -917,7 +918,8 @@ async def test_homekit_unpair_missing_device_id( homekit.driver.aio_stop_event = MagicMock() state = homekit.driver.state - state.add_paired_client("client1", "any", b"1") + client_1 = str(uuid1()).encode("utf-8") + state.add_paired_client(client_1, "any", b"1") with pytest.raises(HomeAssistantError): await hass.services.async_call( DOMAIN, @@ -926,7 +928,7 @@ async def test_homekit_unpair_missing_device_id( blocking=True, ) await hass.async_block_till_done() - state.paired_clients = {"client1": "any"} + state.paired_clients = {client_1.decode("utf-8"): "any"} homekit.status = STATUS_STOPPED @@ -967,7 +969,8 @@ async def test_homekit_unpair_not_homekit_device( ) state = homekit.driver.state - state.add_paired_client("client1", "any", b"1") + client_1 = str(uuid1()).encode("utf-8") + state.add_paired_client(client_1, "any", b"1") with pytest.raises(HomeAssistantError): await hass.services.async_call( DOMAIN, @@ -976,7 +979,7 @@ async def test_homekit_unpair_not_homekit_device( blocking=True, ) await hass.async_block_till_done() - state.paired_clients = {"client1": "any"} + state.paired_clients = {client_1.decode("utf-8"): "any"} homekit.status = STATUS_STOPPED diff --git a/tests/components/knx/test_device_trigger.py b/tests/components/knx/test_device_trigger.py index c7063997585..c3d3ed67b03 100644 --- a/tests/components/knx/test_device_trigger.py +++ b/tests/components/knx/test_device_trigger.py @@ -56,6 +56,7 @@ async def test_if_fires_on_telegram( identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")} ) + # "id" field added to action to test if `trigger_data` passed correctly in `async_attach_trigger` assert await async_setup_component( hass, automation.DOMAIN, @@ -71,7 +72,8 @@ async def test_if_fires_on_telegram( "action": { "service": "test.automation", "data_template": { - "catch_all": ("telegram - {{ trigger.destination }}") + "catch_all": ("telegram - {{ trigger.destination }}"), + "id": (" {{ trigger.id }}"), }, }, }, @@ -82,11 +84,13 @@ async def test_if_fires_on_telegram( "device_id": device_entry.id, "type": "telegram", "destination": ["1/2/3", "1/2/4"], + "id": "test-id", }, "action": { "service": "test.automation", "data_template": { - "specific": ("telegram - {{ trigger.destination }}") + "specific": ("telegram - {{ trigger.destination }}"), + "id": (" {{ trigger.id }}"), }, }, }, @@ -96,12 +100,18 @@ async def test_if_fires_on_telegram( await knx.receive_write("0/0/1", (0x03, 0x2F)) assert len(calls) == 1 - assert calls.pop().data["catch_all"] == "telegram - 0/0/1" + test_call = calls.pop() + assert test_call.data["catch_all"] == "telegram - 0/0/1" + assert test_call.data["id"] == 0 await knx.receive_write("1/2/4", (0x03, 0x2F)) assert len(calls) == 2 - assert calls.pop().data["specific"] == "telegram - 1/2/4" - assert calls.pop().data["catch_all"] == "telegram - 1/2/4" + test_call = calls.pop() + assert test_call.data["specific"] == "telegram - 1/2/4" + assert test_call.data["id"] == "test-id" + test_call = calls.pop() + assert test_call.data["catch_all"] == "telegram - 1/2/4" + assert test_call.data["id"] == 0 async def test_remove_device_trigger( diff --git a/tests/components/totalconnect/common.py b/tests/components/totalconnect/common.py index 54f321c6770..ccee4c43781 100644 --- a/tests/components/totalconnect/common.py +++ b/tests/components/totalconnect/common.py @@ -189,14 +189,23 @@ ZONE_5 = { # 99 is an unknown ZoneType ZONE_6 = { "ZoneID": "6", - "ZoneDescription": "Medical", + "ZoneDescription": "Unknown", "ZoneStatus": ZoneStatus.NORMAL, "ZoneTypeId": 99, "PartitionId": "1", "CanBeBypassed": 0, } -ZONE_INFO = [ZONE_NORMAL, ZONE_2, ZONE_3, ZONE_4, ZONE_5, ZONE_6] +ZONE_7 = { + "ZoneID": 7, + "ZoneDescription": "Temperature", + "ZoneStatus": ZoneStatus.NORMAL, + "ZoneTypeId": ZoneType.MONITOR, + "PartitionId": "1", + "CanBeBypassed": 0, +} + +ZONE_INFO = [ZONE_NORMAL, ZONE_2, ZONE_3, ZONE_4, ZONE_5, ZONE_6, ZONE_7] ZONES = {"ZoneInfo": ZONE_INFO} METADATA_DISARMED = { diff --git a/tests/components/totalconnect/test_binary_sensor.py b/tests/components/totalconnect/test_binary_sensor.py index 966daeb5a63..8f9cabe670c 100644 --- a/tests/components/totalconnect/test_binary_sensor.py +++ b/tests/components/totalconnect/test_binary_sensor.py @@ -84,3 +84,21 @@ async def test_state_and_attributes(hass: HomeAssistant) -> None: assert state.state == STATE_OFF state = hass.states.get("binary_sensor.gas_tamper") assert state.state == STATE_ON + + # Zone 6 is unknown type, assume it is a security (door) sensor + state = hass.states.get("binary_sensor.unknown") + assert state.state == STATE_OFF + assert state.attributes.get("device_class") == BinarySensorDeviceClass.DOOR + state = hass.states.get("binary_sensor.unknown_low_battery") + assert state.state == STATE_OFF + state = hass.states.get("binary_sensor.unknown_tamper") + assert state.state == STATE_OFF + + # Zone 7 is temperature + state = hass.states.get("binary_sensor.temperature") + assert state.state == STATE_OFF + assert state.attributes.get("device_class") == BinarySensorDeviceClass.PROBLEM + state = hass.states.get("binary_sensor.temperature_low_battery") + assert state.state == STATE_OFF + state = hass.states.get("binary_sensor.temperature_tamper") + assert state.state == STATE_OFF diff --git a/tests/components/youtube/fixtures/thumbnail/default.json b/tests/components/youtube/fixtures/thumbnail/default.json new file mode 100644 index 00000000000..6b5d66d6501 --- /dev/null +++ b/tests/components/youtube/fixtures/thumbnail/default.json @@ -0,0 +1,42 @@ +{ + "kind": "youtube#playlistItemListResponse", + "etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8", + "nextPageToken": "EAAaBlBUOkNBVQ", + "items": [ + { + "kind": "youtube#playlistItem", + "etag": "qgpoAJRNskzLhD99njC8e2kPB0M", + "id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Lnd5c3VrRHJNZHFV", + "snippet": { + "publishedAt": "2023-05-11T00:20:46Z", + "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", + "title": "What's new in Google Home in less than 1 minute", + "description": "Discover how your connected devices can do more with Google Home using Matter and Automations at Google I/O 2023.\n\nTo learn more about what's new in Google Home, check out the keynote → https://goo.gle/IO23_homekey\n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#GoogleIO #GoogleHome", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/wysukDrMdqU/default.jpg", + "width": 120, + "height": 90 + } + }, + "channelTitle": "Google for Developers", + "playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw", + "position": 0, + "resourceId": { + "kind": "youtube#video", + "videoId": "wysukDrMdqU" + }, + "videoOwnerChannelTitle": "Google for Developers", + "videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" + }, + "contentDetails": { + "videoId": "wysukDrMdqU", + "videoPublishedAt": "2023-05-11T00:20:46Z" + } + } + ], + "pageInfo": { + "totalResults": 5798, + "resultsPerPage": 1 + } +} diff --git a/tests/components/youtube/fixtures/thumbnail/high.json b/tests/components/youtube/fixtures/thumbnail/high.json new file mode 100644 index 00000000000..430ad3715cc --- /dev/null +++ b/tests/components/youtube/fixtures/thumbnail/high.json @@ -0,0 +1,52 @@ +{ + "kind": "youtube#playlistItemListResponse", + "etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8", + "nextPageToken": "EAAaBlBUOkNBVQ", + "items": [ + { + "kind": "youtube#playlistItem", + "etag": "qgpoAJRNskzLhD99njC8e2kPB0M", + "id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Lnd5c3VrRHJNZHFV", + "snippet": { + "publishedAt": "2023-05-11T00:20:46Z", + "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", + "title": "What's new in Google Home in less than 1 minute", + "description": "Discover how your connected devices can do more with Google Home using Matter and Automations at Google I/O 2023.\n\nTo learn more about what's new in Google Home, check out the keynote → https://goo.gle/IO23_homekey\n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#GoogleIO #GoogleHome", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/wysukDrMdqU/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/wysukDrMdqU/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/wysukDrMdqU/hqdefault.jpg", + "width": 480, + "height": 360 + } + }, + "channelTitle": "Google for Developers", + "playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw", + "position": 0, + "resourceId": { + "kind": "youtube#video", + "videoId": "wysukDrMdqU" + }, + "videoOwnerChannelTitle": "Google for Developers", + "videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" + }, + "contentDetails": { + "videoId": "wysukDrMdqU", + "videoPublishedAt": "2023-05-11T00:20:46Z" + } + } + ], + "pageInfo": { + "totalResults": 5798, + "resultsPerPage": 1 + } +} diff --git a/tests/components/youtube/fixtures/thumbnail/medium.json b/tests/components/youtube/fixtures/thumbnail/medium.json new file mode 100644 index 00000000000..21cb09bd886 --- /dev/null +++ b/tests/components/youtube/fixtures/thumbnail/medium.json @@ -0,0 +1,47 @@ +{ + "kind": "youtube#playlistItemListResponse", + "etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8", + "nextPageToken": "EAAaBlBUOkNBVQ", + "items": [ + { + "kind": "youtube#playlistItem", + "etag": "qgpoAJRNskzLhD99njC8e2kPB0M", + "id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Lnd5c3VrRHJNZHFV", + "snippet": { + "publishedAt": "2023-05-11T00:20:46Z", + "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", + "title": "What's new in Google Home in less than 1 minute", + "description": "Discover how your connected devices can do more with Google Home using Matter and Automations at Google I/O 2023.\n\nTo learn more about what's new in Google Home, check out the keynote → https://goo.gle/IO23_homekey\n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#GoogleIO #GoogleHome", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/wysukDrMdqU/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/wysukDrMdqU/mqdefault.jpg", + "width": 320, + "height": 180 + } + }, + "channelTitle": "Google for Developers", + "playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw", + "position": 0, + "resourceId": { + "kind": "youtube#video", + "videoId": "wysukDrMdqU" + }, + "videoOwnerChannelTitle": "Google for Developers", + "videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" + }, + "contentDetails": { + "videoId": "wysukDrMdqU", + "videoPublishedAt": "2023-05-11T00:20:46Z" + } + } + ], + "pageInfo": { + "totalResults": 5798, + "resultsPerPage": 1 + } +} diff --git a/tests/components/youtube/fixtures/thumbnail/none.json b/tests/components/youtube/fixtures/thumbnail/none.json new file mode 100644 index 00000000000..d4c28730cab --- /dev/null +++ b/tests/components/youtube/fixtures/thumbnail/none.json @@ -0,0 +1,36 @@ +{ + "kind": "youtube#playlistItemListResponse", + "etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8", + "nextPageToken": "EAAaBlBUOkNBVQ", + "items": [ + { + "kind": "youtube#playlistItem", + "etag": "qgpoAJRNskzLhD99njC8e2kPB0M", + "id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Lnd5c3VrRHJNZHFV", + "snippet": { + "publishedAt": "2023-05-11T00:20:46Z", + "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", + "title": "What's new in Google Home in less than 1 minute", + "description": "Discover how your connected devices can do more with Google Home using Matter and Automations at Google I/O 2023.\n\nTo learn more about what's new in Google Home, check out the keynote → https://goo.gle/IO23_homekey\n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#GoogleIO #GoogleHome", + "thumbnails": {}, + "channelTitle": "Google for Developers", + "playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw", + "position": 0, + "resourceId": { + "kind": "youtube#video", + "videoId": "wysukDrMdqU" + }, + "videoOwnerChannelTitle": "Google for Developers", + "videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" + }, + "contentDetails": { + "videoId": "wysukDrMdqU", + "videoPublishedAt": "2023-05-11T00:20:46Z" + } + } + ], + "pageInfo": { + "totalResults": 5798, + "resultsPerPage": 1 + } +} diff --git a/tests/components/youtube/fixtures/thumbnail/standard.json b/tests/components/youtube/fixtures/thumbnail/standard.json new file mode 100644 index 00000000000..bdbedfcf4c9 --- /dev/null +++ b/tests/components/youtube/fixtures/thumbnail/standard.json @@ -0,0 +1,57 @@ +{ + "kind": "youtube#playlistItemListResponse", + "etag": "O0Ah8Wd5pUD2Gsv-n0A42RDRcX8", + "nextPageToken": "EAAaBlBUOkNBVQ", + "items": [ + { + "kind": "youtube#playlistItem", + "etag": "qgpoAJRNskzLhD99njC8e2kPB0M", + "id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Lnd5c3VrRHJNZHFV", + "snippet": { + "publishedAt": "2023-05-11T00:20:46Z", + "channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw", + "title": "What's new in Google Home in less than 1 minute", + "description": "Discover how your connected devices can do more with Google Home using Matter and Automations at Google I/O 2023.\n\nTo learn more about what's new in Google Home, check out the keynote → https://goo.gle/IO23_homekey\n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#GoogleIO #GoogleHome", + "thumbnails": { + "default": { + "url": "https://i.ytimg.com/vi/wysukDrMdqU/default.jpg", + "width": 120, + "height": 90 + }, + "medium": { + "url": "https://i.ytimg.com/vi/wysukDrMdqU/mqdefault.jpg", + "width": 320, + "height": 180 + }, + "high": { + "url": "https://i.ytimg.com/vi/wysukDrMdqU/hqdefault.jpg", + "width": 480, + "height": 360 + }, + "standard": { + "url": "https://i.ytimg.com/vi/wysukDrMdqU/sddefault.jpg", + "width": 640, + "height": 480 + } + }, + "channelTitle": "Google for Developers", + "playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw", + "position": 0, + "resourceId": { + "kind": "youtube#video", + "videoId": "wysukDrMdqU" + }, + "videoOwnerChannelTitle": "Google for Developers", + "videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw" + }, + "contentDetails": { + "videoId": "wysukDrMdqU", + "videoPublishedAt": "2023-05-11T00:20:46Z" + } + } + ], + "pageInfo": { + "totalResults": 5798, + "resultsPerPage": 1 + } +} diff --git a/tests/components/youtube/test_sensor.py b/tests/components/youtube/test_sensor.py index 3462e291af8..6bd99399952 100644 --- a/tests/components/youtube/test_sensor.py +++ b/tests/components/youtube/test_sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import patch from google.auth.exceptions import RefreshError +import pytest from homeassistant import config_entries from homeassistant.components.youtube import DOMAIN @@ -87,3 +88,38 @@ async def test_sensor_reauth_trigger( assert flow["step_id"] == "reauth_confirm" assert flow["handler"] == DOMAIN assert flow["context"]["source"] == config_entries.SOURCE_REAUTH + + +@pytest.mark.parametrize( + ("fixture", "url", "has_entity_picture"), + [ + ("standard", "https://i.ytimg.com/vi/wysukDrMdqU/sddefault.jpg", True), + ("high", "https://i.ytimg.com/vi/wysukDrMdqU/hqdefault.jpg", True), + ("medium", "https://i.ytimg.com/vi/wysukDrMdqU/mqdefault.jpg", True), + ("default", "https://i.ytimg.com/vi/wysukDrMdqU/default.jpg", True), + ("none", None, False), + ], +) +async def test_thumbnail( + hass: HomeAssistant, + setup_integration: ComponentSetup, + fixture: str, + url: str | None, + has_entity_picture: bool, +) -> None: + """Test if right thumbnail is selected.""" + await setup_integration() + + with patch( + "homeassistant.components.youtube.api.build", + return_value=MockService( + playlist_items_fixture=f"youtube/thumbnail/{fixture}.json" + ), + ): + future = dt_util.utcnow() + timedelta(minutes=15) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + state = hass.states.get("sensor.google_for_developers_latest_upload") + assert state + assert ("entity_picture" in state.attributes) is has_entity_picture + assert state.attributes.get("entity_picture") == url diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index e7d7d9594bd..aa5ec77b798 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -18,11 +18,11 @@ from homeassistant.components.zwave_js.helpers import ( get_value_id_from_unique_id, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import async_get as async_get_dev_reg -from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg +from homeassistant.helpers import device_registry as dr, entity_registry as er from .common import PROPERTY_ULTRAVIOLET +from tests.common import MockConfigEntry from tests.components.diagnostics import ( get_diagnostics_for_config_entry, get_diagnostics_for_device, @@ -57,10 +57,26 @@ async def test_device_diagnostics( version_state, ) -> None: """Test the device level diagnostics data dump.""" - dev_reg = async_get_dev_reg(hass) + dev_reg = dr.async_get(hass) device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)}) assert device + # Create mock config entry for fake entity + mock_config_entry = MockConfigEntry(domain="test_integration") + mock_config_entry.add_to_hass(hass) + + # Add an entity entry to the device that is not part of this config entry + ent_reg = er.async_get(hass) + ent_reg.async_get_or_create( + "test", + "test_integration", + "test_unique_id", + suggested_object_id="unrelated_entity", + config_entry=mock_config_entry, + device_id=device.id, + ) + assert ent_reg.async_get("test.unrelated_entity") + # Update a value and ensure it is reflected in the node state event = Event( type="value updated", @@ -92,16 +108,27 @@ async def test_device_diagnostics( } # Assert that we only have the entities that were discovered for this device # Entities that are created outside of discovery (e.g. node status sensor and - # ping button) should not be in dump. + # ping button) as well as helper entities created from other integrations should + # not be in dump. assert len(diagnostics_data["entities"]) == len( list(async_discover_node_values(multisensor_6, device, {device.id: set()})) ) + assert any( + entity.entity_id == "test.unrelated_entity" + for entity in er.async_entries_for_device(ent_reg, device.id) + ) + # Explicitly check that the entity that is not part of this config entry is not + # in the dump. + assert not any( + entity["entity_id"] == "test.unrelated_entity" + for entity in diagnostics_data["entities"] + ) assert diagnostics_data["state"] == multisensor_6.data async def test_device_diagnostics_error(hass: HomeAssistant, integration) -> None: """Test the device diagnostics raises exception when an invalid device is used.""" - dev_reg = async_get_dev_reg(hass) + dev_reg = dr.async_get(hass) device = dev_reg.async_get_or_create( config_entry_id=integration.entry_id, identifiers={("test", "test")} ) @@ -123,12 +150,12 @@ async def test_device_diagnostics_missing_primary_value( hass_client: ClientSessionGenerator, ) -> None: """Test that device diagnostics handles an entity with a missing primary value.""" - dev_reg = async_get_dev_reg(hass) + dev_reg = dr.async_get(hass) device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)}) assert device entity_id = "sensor.multisensor_6_air_temperature" - ent_reg = async_get_ent_reg(hass) + ent_reg = er.async_get(hass) entry = ent_reg.async_get(entity_id) # check that the primary value for the entity exists in the diagnostics @@ -212,7 +239,7 @@ async def test_device_diagnostics_secret_value( client.driver.controller.nodes[node.node_id] = node client.driver.controller.emit("node added", {"node": node}) await hass.async_block_till_done() - dev_reg = async_get_dev_reg(hass) + dev_reg = dr.async_get(hass) device = dev_reg.async_get_device({get_device_id(client.driver, node)}) assert device diff --git a/tests/components/zwave_js/test_trigger.py b/tests/components/zwave_js/test_trigger.py index 0fb3b829d9a..eae9d6f5416 100644 --- a/tests/components/zwave_js/test_trigger.py +++ b/tests/components/zwave_js/test_trigger.py @@ -1112,20 +1112,21 @@ def test_get_trigger_platform_failure() -> None: async def test_server_reconnect_event( - hass: HomeAssistant, client, lock_schlage_be469, integration + hass: HomeAssistant, + client, + lock_schlage_be469, + lock_schlage_be469_state, + integration, ) -> None: """Test that when we reconnect to server, event triggers reattach.""" trigger_type = f"{DOMAIN}.event" - node: Node = lock_schlage_be469 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} - ) - assert device + old_node: Node = lock_schlage_be469 event_name = "interview stage completed" - original_len = len(node._listeners.get(event_name, [])) + old_node = client.driver.controller.nodes[20] + + original_len = len(old_node._listeners.get(event_name, [])) assert await async_setup_component( hass, @@ -1147,34 +1148,65 @@ async def test_server_reconnect_event( }, ) - assert len(node._listeners.get(event_name, [])) == original_len + 1 - old_listener = node._listeners.get(event_name, [])[original_len] + assert len(old_node._listeners.get(event_name, [])) == original_len + 1 + old_listener = old_node._listeners.get(event_name, [])[original_len] + # Remove node so that we can create a new node instance and make sure the listener + # attaches + node_removed_event = Event( + type="node removed", + data={ + "source": "controller", + "event": "node removed", + "replaced": False, + "node": lock_schlage_be469_state, + }, + ) + client.driver.controller.receive_event(node_removed_event) + assert 20 not in client.driver.controller.nodes + await hass.async_block_till_done() + + # Add node like new server connection would + node_added_event = Event( + type="node added", + data={ + "source": "controller", + "event": "node added", + "node": lock_schlage_be469_state, + "result": {}, + }, + ) + client.driver.controller.receive_event(node_added_event) + await hass.async_block_till_done() + + # Reload integration to trigger the dispatch signal await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() - # Make sure there is still a listener added for the trigger - assert len(node._listeners.get(event_name, [])) == original_len + 1 + # Make sure there is a listener added for the trigger to the new node + new_node = client.driver.controller.nodes[20] + assert len(new_node._listeners.get(event_name, [])) == original_len + 1 - # Make sure the old listener was removed - assert old_listener not in node._listeners.get(event_name, []) + # Make sure the old listener is no longer referenced + assert old_listener not in new_node._listeners.get(event_name, []) async def test_server_reconnect_value_updated( - hass: HomeAssistant, client, lock_schlage_be469, integration + hass: HomeAssistant, + client, + lock_schlage_be469, + lock_schlage_be469_state, + integration, ) -> None: """Test that when we reconnect to server, value_updated triggers reattach.""" trigger_type = f"{DOMAIN}.value_updated" - node: Node = lock_schlage_be469 - dev_reg = async_get_dev_reg(hass) - device = dev_reg.async_get_device( - {get_device_id(client.driver, lock_schlage_be469)} - ) - assert device + old_node: Node = lock_schlage_be469 event_name = "value updated" - original_len = len(node._listeners.get(event_name, [])) + old_node = client.driver.controller.nodes[20] + + original_len = len(old_node._listeners.get(event_name, [])) assert await async_setup_component( hass, @@ -1196,14 +1228,44 @@ async def test_server_reconnect_value_updated( }, ) - assert len(node._listeners.get(event_name, [])) == original_len + 1 - old_listener = node._listeners.get(event_name, [])[original_len] + assert len(old_node._listeners.get(event_name, [])) == original_len + 1 + old_listener = old_node._listeners.get(event_name, [])[original_len] + # Remove node so that we can create a new node instance and make sure the listener + # attaches + node_removed_event = Event( + type="node removed", + data={ + "source": "controller", + "event": "node removed", + "replaced": False, + "node": lock_schlage_be469_state, + }, + ) + client.driver.controller.receive_event(node_removed_event) + assert 20 not in client.driver.controller.nodes + await hass.async_block_till_done() + + # Add node like new server connection would + node_added_event = Event( + type="node added", + data={ + "source": "controller", + "event": "node added", + "node": lock_schlage_be469_state, + "result": {}, + }, + ) + client.driver.controller.receive_event(node_added_event) + await hass.async_block_till_done() + + # Reload integration to trigger the dispatch signal await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() - # Make sure there is still a listener added for the trigger - assert len(node._listeners.get(event_name, [])) == original_len + 1 + # Make sure there is a listener added for the trigger to the new node + new_node = client.driver.controller.nodes[20] + assert len(new_node._listeners.get(event_name, [])) == original_len + 1 - # Make sure the old listener was removed - assert old_listener not in node._listeners.get(event_name, []) + # Make sure the old listener is no longer referenced + assert old_listener not in new_node._listeners.get(event_name, [])