Compare commits

...

16 Commits

Author SHA1 Message Date
mettolen
87b9c3193e Add sensor entities to Airobot integration (#157938) 2025-12-06 07:57:03 +01:00
Adam Goode
061c38d2a7 Make unifi LEDs EntityCategory.CONFIG (#158088) 2025-12-06 07:51:09 +01:00
Allen Porter
e1720be5a4 Update roborock quality scale (#158024) 2025-12-05 22:52:38 +01:00
Paul Bottein
2d13a92496 Update frontend to 20251203.1 (#158069) 2025-12-05 21:25:01 +01:00
Artur Pragacz
b06bffa815 Add ai_task to core files (#158058) 2025-12-05 21:14:49 +01:00
Joost Lekkerkerker
b8f4b9515b Prevent entsoe from loading (#158036)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-05 21:08:57 +01:00
Petro31
3c10e9f1c0 Fix inverted kelvin issue (#158054) 2025-12-05 19:12:12 +00:00
Artur Pragacz
2dec3befcd Assign hass in Condition init (#158062) 2025-12-05 19:04:11 +00:00
J. Nick Koston
7d065bf314 Bump aiodns to 3.6.0 (#158063) 2025-12-05 20:00:09 +01:00
Raphael Hehl
3315680d0b Bump uiprotect to 7.33.2 (#158057) 2025-12-05 19:43:44 +01:00
Markus Jacobsen
ce48c89a26 Fix button event entity creation in Bang & Olufsen (#157982)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-12-05 18:39:58 +00:00
Jan Bouwhuis
f67a926f56 Move lametric URLs out of strings.json (#158051) 2025-12-05 19:35:07 +01:00
Paulus Schoutsen
e0a9d305b2 Use multiple selector for validation in AI task (#158056) 2025-12-05 18:51:18 +01:00
Jan Bouwhuis
4ff141d35e Move example image path out of translatable strings (#158053) 2025-12-05 18:05:09 +01:00
Artur Pragacz
f12a43b2b7 Mark reauthentication in music assistant quality scale (#158055) 2025-12-05 18:02:16 +01:00
Paul Tarjan
35e6f504a3 Fix doorbird duplicate unique ID generation (#158013)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-05 10:17:43 -06:00
51 changed files with 1002 additions and 217 deletions

View File

@@ -13,6 +13,7 @@ core: &core
# Our base platforms, that are used by other integrations
base_platforms: &base_platforms
- homeassistant/components/ai_task/**
- homeassistant/components/air_quality/**
- homeassistant/components/alarm_control_panel/**
- homeassistant/components/assist_satellite/**

View File

@@ -101,8 +101,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
vol.Schema({str: STRUCTURE_FIELD_SCHEMA}),
_validate_structure_fields,
),
vol.Optional(ATTR_ATTACHMENTS): vol.All(
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
vol.Optional(ATTR_ATTACHMENTS): selector.MediaSelector(
{"accept": ["*/*"], "multiple": True}
),
}
),
@@ -118,8 +118,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
vol.Required(ATTR_TASK_NAME): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_INSTRUCTIONS): cv.string,
vol.Optional(ATTR_ATTACHMENTS): vol.All(
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
vol.Optional(ATTR_ATTACHMENTS): selector.MediaSelector(
{"accept": ["*/*"], "multiple": True}
),
}
),

View File

@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.CLIMATE]
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: AirobotConfigEntry) -> bool:

View File

@@ -44,7 +44,7 @@ rules:
discovery: done
docs-data-update: done
docs-examples: todo
docs-known-limitations: todo
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
@@ -54,7 +54,7 @@ rules:
comment: Single device integration, no dynamic device discovery needed.
entity-category: done
entity-device-class: done
entity-disabled-by-default: todo
entity-disabled-by-default: done
entity-translations: todo
exception-translations: done
icon-translations: todo

View File

@@ -0,0 +1,134 @@
"""Sensor platform for Airobot thermostat."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from pyairobotrest.models import ThermostatStatus
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
EntityCategory,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import AirobotConfigEntry
from .entity import AirobotEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AirobotSensorEntityDescription(SensorEntityDescription):
"""Describes Airobot sensor entity."""
value_fn: Callable[[ThermostatStatus], StateType]
supported_fn: Callable[[ThermostatStatus], bool] = lambda _: True
SENSOR_TYPES: tuple[AirobotSensorEntityDescription, ...] = (
AirobotSensorEntityDescription(
key="air_temperature",
translation_key="air_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.temp_air,
),
AirobotSensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.hum_air,
),
AirobotSensorEntityDescription(
key="floor_temperature",
translation_key="floor_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.temp_floor,
supported_fn=lambda status: status.has_floor_sensor,
),
AirobotSensorEntityDescription(
key="co2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.co2,
supported_fn=lambda status: status.has_co2_sensor,
),
AirobotSensorEntityDescription(
key="air_quality_index",
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.aqi,
supported_fn=lambda status: status.has_co2_sensor,
),
AirobotSensorEntityDescription(
key="heating_uptime",
translation_key="heating_uptime",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda status: status.heating_uptime,
entity_registry_enabled_default=False,
),
AirobotSensorEntityDescription(
key="errors",
translation_key="errors",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda status: status.errors,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AirobotConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Airobot sensor platform."""
coordinator = entry.runtime_data
async_add_entities(
AirobotSensor(coordinator, description)
for description in SENSOR_TYPES
if description.supported_fn(coordinator.data.status)
)
class AirobotSensor(AirobotEntity, SensorEntity):
"""Representation of an Airobot sensor."""
entity_description: AirobotSensorEntityDescription
def __init__(
self,
coordinator,
description: AirobotSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.data.status.device_id}_{description.key}"
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data.status)

View File

@@ -43,6 +43,25 @@
}
}
},
"entity": {
"sensor": {
"air_temperature": {
"name": "Air temperature"
},
"device_uptime": {
"name": "Device uptime"
},
"errors": {
"name": "Error count"
},
"floor_temperature": {
"name": "Floor temperature"
},
"heating_uptime": {
"name": "Heating uptime"
}
}
},
"exceptions": {
"authentication_failed": {
"message": "Authentication failed, please reauthenticate."

View File

@@ -42,14 +42,25 @@ async def get_remotes(client: MozartClient) -> list[PairedRemote]:
def get_device_buttons(model: BeoModel) -> list[str]:
"""Get supported buttons for a given model."""
# Beoconnect Core does not have any buttons
if model == BeoModel.BEOCONNECT_CORE:
return []
buttons = DEVICE_BUTTONS.copy()
# Beosound Premiere does not have a bluetooth button
if model == BeoModel.BEOSOUND_PREMIERE:
# Models that don't have a microphone button
if model in (
BeoModel.BEOSOUND_A5,
BeoModel.BEOSOUND_A9,
BeoModel.BEOSOUND_PREMIERE,
):
buttons.remove(BeoButtons.MICROPHONE)
# Models that don't have a Bluetooth button
if model in (
BeoModel.BEOSOUND_A9,
BeoModel.BEOSOUND_PREMIERE,
):
buttons.remove(BeoButtons.BLUETOOTH)
# Beoconnect Core does not have any buttons
elif model == BeoModel.BEOCONNECT_CORE:
buttons = []
return buttons

View File

@@ -56,7 +56,6 @@ class DeviceAutomationConditionProtocol(Protocol):
class DeviceCondition(Condition):
"""Device condition."""
_hass: HomeAssistant
_config: ConfigType
@classmethod
@@ -87,7 +86,7 @@ class DeviceCondition(Condition):
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
"""Initialize condition."""
self._hass = hass
super().__init__(hass, config)
assert config.options is not None
self._config = config.options

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"iot_class": "cloud_polling",
"requirements": ["aiodns==3.5.0"]
"requirements": ["aiodns==3.6.0"]
}

View File

@@ -102,6 +102,12 @@ class ConfiguredDoorBird:
"""Get token for device."""
return self._token
def _get_hass_url(self) -> str:
"""Get the Home Assistant URL for this device."""
if custom_url := self.custom_url:
return custom_url
return get_url(self._hass, prefer_external=False)
async def async_register_events(self) -> None:
"""Register events on device."""
if not self.door_station_events:
@@ -146,13 +152,7 @@ class ConfiguredDoorBird:
async def _async_register_events(self) -> dict[str, Any]:
"""Register events on device."""
# Override url if another is specified in the configuration
if custom_url := self.custom_url:
hass_url = custom_url
else:
# Get the URL of this server
hass_url = get_url(self._hass, prefer_external=False)
hass_url = self._get_hass_url()
http_fav = await self._async_get_http_favorites()
if any(
# Note that a list comp is used here to ensure all
@@ -191,10 +191,14 @@ class ConfiguredDoorBird:
self._get_event_name(event): event_type
for event, event_type in DEFAULT_EVENT_TYPES
}
hass_url = self._get_hass_url()
for identifier, data in http_fav.items():
title: str | None = data.get("title")
if not title or not title.startswith("Home Assistant"):
continue
value: str | None = data.get("value")
if not value or not value.startswith(hass_url):
continue # Not our favorite - different HA instance or stale
event = title.partition("(")[2].strip(")")
if input_type := favorite_input_type.get(identifier):
events.append(DoorbirdEvent(event, input_type))

View File

@@ -23,5 +23,5 @@
"winter_mode": {}
},
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20251203.0"]
"requirements": ["home-assistant-frontend==20251203.1"]
}

View File

@@ -149,6 +149,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
}
),
supports_response=SupportsResponse.ONLY,
description_placeholders={"example_image_path": "/config/www/image.jpg"},
)
return True

View File

@@ -162,7 +162,7 @@
"fields": {
"filenames": {
"description": "Attachments to add to the prompt (images, PDFs, etc)",
"example": "/config/www/image.jpg",
"example": "{example_image_path}",
"name": "Attachment filenames"
},
"prompt": {

View File

@@ -159,4 +159,5 @@ def async_setup_services(hass: HomeAssistant) -> None:
_async_handle_upload,
schema=UPLOAD_SERVICE_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
description_placeholders={"example_image_path": "/config/www/image.jpg"},
)

View File

@@ -92,7 +92,7 @@
},
"filename": {
"description": "Path to the image or video to upload.",
"example": "/config/www/image.jpg",
"example": "{example_image_path}",
"name": "Filename"
}
},

View File

@@ -108,6 +108,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
SERVICE_MESSAGE,
_async_service_message,
schema=SERVICE_MESSAGE_SCHEMA,
description_placeholders={"icons_url": "https://developer.lametric.com/icons"},
)

View File

@@ -211,7 +211,7 @@
"name": "[%key:common::config_flow::data::device%]"
},
"icon": {
"description": "The ID number of the icon or animation to display. List of all icons and their IDs can be found at: https://developer.lametric.com/icons.",
"description": "The ID number of the icon or animation to display. List of all icons and their IDs can be found at: {icons_url}.",
"name": "Icon ID"
},
"icon_type": {

View File

@@ -52,7 +52,7 @@ class StateConditionBase(Condition):
self, hass: HomeAssistant, config: ConditionConfig, state: str
) -> None:
"""Initialize condition."""
self._hass = hass
super().__init__(hass, config)
if TYPE_CHECKING:
assert config.target
assert config.options

View File

@@ -30,9 +30,7 @@ rules:
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow:
status: exempt
comment: Devices don't require authentication
reauthentication-flow: done
test-coverage: todo
# Gold

View File

@@ -129,4 +129,5 @@ def async_setup_services(hass: HomeAssistant) -> None:
async_handle_upload,
schema=UPLOAD_SERVICE_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
description_placeholders={"example_image_path": "/config/www/image.jpg"},
)

View File

@@ -156,7 +156,7 @@
},
"filename": {
"description": "Path to the file to upload.",
"example": "/config/www/image.jpg",
"example": "{example_image_path}",
"name": "Filename"
}
},

View File

@@ -65,11 +65,9 @@ rules:
exception-translations: done
icon-translations: todo
reconfiguration-flow: todo
repair-issues:
status: todo
comment: The Cloud vs Local API warning should probably be a repair issue.
repair-issues: done
stale-devices: done
# Platinum
async-dependency: todo
async-dependency: done
inject-websession: done
strict-typing: done

View File

@@ -150,6 +150,7 @@ class SunCondition(Condition):
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
"""Initialize condition."""
super().__init__(hass, config)
assert config.options is not None
self._options = config.options

View File

@@ -635,14 +635,14 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
# Support legacy mireds in template light.
temperature = int(render)
if (min_kelvin := self._attr_min_color_temp_kelvin) is not None:
min_mireds = color_util.color_temperature_kelvin_to_mired(min_kelvin)
else:
min_mireds = DEFAULT_MIN_MIREDS
if (max_kelvin := self._attr_max_color_temp_kelvin) is not None:
max_mireds = color_util.color_temperature_kelvin_to_mired(max_kelvin)
max_mireds = color_util.color_temperature_kelvin_to_mired(min_kelvin)
else:
max_mireds = DEFAULT_MAX_MIREDS
if (max_kelvin := self._attr_max_color_temp_kelvin) is not None:
min_mireds = color_util.color_temperature_kelvin_to_mired(max_kelvin)
else:
min_mireds = DEFAULT_MIN_MIREDS
if min_mireds <= temperature <= max_mireds:
self._attr_color_temp_kelvin = (
color_util.color_temperature_mired_to_kelvin(temperature)
@@ -856,42 +856,36 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
try:
if render in (None, "None", ""):
self._attr_max_mireds = DEFAULT_MAX_MIREDS
self._attr_max_color_temp_kelvin = None
self._attr_min_color_temp_kelvin = None
return
self._attr_max_mireds = max_mireds = int(render)
self._attr_max_color_temp_kelvin = (
color_util.color_temperature_mired_to_kelvin(max_mireds)
self._attr_min_color_temp_kelvin = (
color_util.color_temperature_mired_to_kelvin(int(render))
)
except ValueError:
_LOGGER.exception(
"Template must supply an integer temperature within the range for"
" this light, or 'None'"
)
self._attr_max_mireds = DEFAULT_MAX_MIREDS
self._attr_max_color_temp_kelvin = None
self._attr_min_color_temp_kelvin = None
@callback
def _update_min_mireds(self, render):
"""Update the min mireds from the template."""
try:
if render in (None, "None", ""):
self._attr_min_mireds = DEFAULT_MIN_MIREDS
self._attr_min_color_temp_kelvin = None
self._attr_max_color_temp_kelvin = None
return
self._attr_min_mireds = min_mireds = int(render)
self._attr_min_color_temp_kelvin = (
color_util.color_temperature_mired_to_kelvin(min_mireds)
self._attr_max_color_temp_kelvin = (
color_util.color_temperature_mired_to_kelvin(int(render))
)
except ValueError:
_LOGGER.exception(
"Template must supply an integer temperature within the range for"
" this light, or 'None'"
)
self._attr_min_mireds = DEFAULT_MIN_MIREDS
self._attr_min_color_temp_kelvin = None
self._attr_max_color_temp_kelvin = None
@callback
def _update_supports_transition(self, render):

View File

@@ -19,6 +19,7 @@ from homeassistant.components.light import (
LightEntityDescription,
LightEntityFeature,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.color import rgb_hex_to_rgb_list
@@ -117,6 +118,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiLightEntityDescription, ...] = (
UnifiLightEntityDescription[Devices, Device](
key="LED control",
translation_key="led_control",
entity_category=EntityCategory.CONFIG,
allowed_fn=lambda hub, obj_id: True,
api_handler_fn=lambda api: api.devices,
available_fn=async_device_available_fn,

View File

@@ -40,7 +40,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["uiprotect", "unifi_discovery"],
"requirements": ["uiprotect==7.31.0", "unifi-discovery==1.2.0"],
"requirements": ["uiprotect==7.33.2", "unifi-discovery==1.2.0"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",

View File

@@ -114,6 +114,7 @@ class ZoneCondition(Condition):
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
"""Initialize condition."""
super().__init__(hass, config)
assert config.options is not None
self._options = config.options

View File

@@ -259,6 +259,8 @@ _CONDITION_SCHEMA = _CONDITION_BASE_SCHEMA.extend(
class Condition(abc.ABC):
"""Condition class."""
_hass: HomeAssistant
@classmethod
async def async_validate_complete_config(
cls, hass: HomeAssistant, complete_config: ConfigType
@@ -293,6 +295,7 @@ class Condition(abc.ABC):
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
"""Initialize condition."""
self._hass = hass
@abc.abstractmethod
async def async_get_checker(self) -> ConditionCheckerType:

View File

@@ -124,6 +124,12 @@ BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = {
# Added in 2025.10.0 because of
# https://github.com/frenck/spook/issues/1066
"spook": BlockedIntegration(AwesomeVersion("4.0.0"), "breaks the template engine"),
# Added in 2025.12.1 because of
# https://github.com/JaccoR/hass-entso-e/issues/263
"entsoe": BlockedIntegration(
AwesomeVersion("0.7.1"),
"crashes Home Assistant when it can't connect to the API",
),
}
DATA_COMPONENTS: HassKey[dict[str, ModuleType | ComponentProtocol]] = HassKey(

View File

@@ -2,7 +2,7 @@
aiodhcpwatcher==1.2.1
aiodiscover==2.7.1
aiodns==3.5.0
aiodns==3.6.0
aiohasupervisor==0.3.3
aiohttp-asyncmdnsresolver==0.1.1
aiohttp-fast-zlib==0.3.0
@@ -39,7 +39,7 @@ habluetooth==5.8.0
hass-nabucasa==1.7.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20251203.0
home-assistant-frontend==20251203.1
home-assistant-intents==2025.12.2
httpx==0.28.1
ifaddr==0.2.0

View File

@@ -24,7 +24,7 @@ classifiers = [
]
requires-python = ">=3.13.2"
dependencies = [
"aiodns==3.5.0",
"aiodns==3.6.0",
# Integrations may depend on hassio integration without listing it to
# change behavior based on presence of supervisor. Deprecated with #127228
# Lib can be removed with 2025.11

2
requirements.txt generated
View File

@@ -3,7 +3,7 @@
-c homeassistant/package_constraints.txt
# Home Assistant Core
aiodns==3.5.0
aiodns==3.6.0
aiohasupervisor==0.3.3
aiohttp==3.13.2
aiohttp_cors==0.8.1

6
requirements_all.txt generated
View File

@@ -231,7 +231,7 @@ aiodhcpwatcher==1.2.1
aiodiscover==2.7.1
# homeassistant.components.dnsip
aiodns==3.5.0
aiodns==3.6.0
# homeassistant.components.duke_energy
aiodukeenergy==0.3.0
@@ -1204,7 +1204,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20251203.0
home-assistant-frontend==20251203.1
# homeassistant.components.conversation
home-assistant-intents==2025.12.2
@@ -3059,7 +3059,7 @@ typedmonarchmoney==0.4.4
uasiren==0.0.1
# homeassistant.components.unifiprotect
uiprotect==7.31.0
uiprotect==7.33.2
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7

View File

@@ -222,7 +222,7 @@ aiodhcpwatcher==1.2.1
aiodiscover==2.7.1
# homeassistant.components.dnsip
aiodns==3.5.0
aiodns==3.6.0
# homeassistant.components.duke_energy
aiodukeenergy==0.3.0
@@ -1062,7 +1062,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20251203.0
home-assistant-frontend==20251203.1
# homeassistant.components.conversation
home-assistant-intents==2025.12.2
@@ -2547,7 +2547,7 @@ typedmonarchmoney==0.4.4
uasiren==0.0.1
# homeassistant.components.unifiprotect
uiprotect==7.31.0
uiprotect==7.33.2
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7

View File

@@ -12,7 +12,13 @@ from pyairobotrest.models import (
import pytest
from homeassistant.components.airobot.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
CONF_PASSWORD,
CONF_USERNAME,
Platform,
)
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@@ -105,16 +111,24 @@ def mock_config_entry() -> MockConfigEntry:
)
@pytest.fixture
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""
return [Platform.CLIMATE, Platform.SENSOR]
@pytest.fixture
async def init_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_airobot_client: AsyncMock,
platforms: list[Platform],
) -> MockConfigEntry:
"""Set up the Airobot integration for testing."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
with patch("homeassistant.components.airobot.PLATFORMS", platforms):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry

View File

@@ -0,0 +1,220 @@
# serializer version: 1
# name: test_sensors[sensor.test_thermostat_air_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_thermostat_air_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Air temperature',
'platform': 'airobot',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'air_temperature',
'unique_id': 'T01A1B2C3_air_temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[sensor.test_thermostat_air_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Test Thermostat Air temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_thermostat_air_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '22.0',
})
# ---
# name: test_sensors[sensor.test_thermostat_error_count-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_thermostat_error_count',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Error count',
'platform': 'airobot',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'errors',
'unique_id': 'T01A1B2C3_errors',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[sensor.test_thermostat_error_count-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Thermostat Error count',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_thermostat_error_count',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_sensors[sensor.test_thermostat_heating_uptime-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_thermostat_heating_uptime',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Heating uptime',
'platform': 'airobot',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'heating_uptime',
'unique_id': 'T01A1B2C3_heating_uptime',
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
})
# ---
# name: test_sensors[sensor.test_thermostat_heating_uptime-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'Test Thermostat Heating uptime',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_thermostat_heating_uptime',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1.38888888888889',
})
# ---
# name: test_sensors[sensor.test_thermostat_humidity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_thermostat_humidity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
'original_icon': None,
'original_name': 'Humidity',
'platform': 'airobot',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'T01A1B2C3_humidity',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[sensor.test_thermostat_humidity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'humidity',
'friendly_name': 'Test Thermostat Humidity',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.test_thermostat_humidity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '45.0',
})
# ---

View File

@@ -17,7 +17,7 @@ from homeassistant.components.climate import (
SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE,
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
import homeassistant.helpers.entity_registry as er
@@ -25,12 +25,19 @@ import homeassistant.helpers.entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""
return [Platform.CLIMATE]
@pytest.mark.usefixtures("init_integration")
async def test_climate_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
platforms: list[Platform],
) -> None:
"""Test climate entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)

View File

@@ -0,0 +1,38 @@
"""Tests for the Airobot sensor platform."""
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""
return [Platform.SENSOR]
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_sensors(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the sensor entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_sensor_availability_without_optional_sensors(
hass: HomeAssistant,
) -> None:
"""Test sensors are not created when optional hardware is not present."""
# Default mock has no floor sensor, CO2, or AQI - they should not be created
assert hass.states.get("sensor.test_thermostat_floor_temperature") is None
assert hass.states.get("sensor.test_thermostat_carbon_dioxide") is None
assert hass.states.get("sensor.test_thermostat_air_quality_index") is None

View File

@@ -37,6 +37,7 @@ from .const import (
TEST_DATA_CREATE_ENTRY,
TEST_DATA_CREATE_ENTRY_2,
TEST_DATA_CREATE_ENTRY_3,
TEST_DATA_CREATE_ENTRY_4,
TEST_FRIENDLY_NAME,
TEST_FRIENDLY_NAME_3,
TEST_FRIENDLY_NAME_4,
@@ -48,10 +49,12 @@ from .const import (
TEST_NAME,
TEST_NAME_2,
TEST_NAME_3,
TEST_NAME_4,
TEST_REMOTE_SERIAL,
TEST_SERIAL_NUMBER,
TEST_SERIAL_NUMBER_2,
TEST_SERIAL_NUMBER_3,
TEST_SERIAL_NUMBER_4,
TEST_SOUND_MODE,
TEST_SOUND_MODE_2,
TEST_SOUND_MODE_NAME,
@@ -93,6 +96,17 @@ def mock_config_entry_premiere() -> MockConfigEntry:
)
@pytest.fixture
def mock_config_entry_a5() -> MockConfigEntry:
"""Mock config entry for Beosound A5."""
return MockConfigEntry(
domain=DOMAIN,
unique_id=TEST_SERIAL_NUMBER_4,
data=TEST_DATA_CREATE_ENTRY_4,
title=TEST_NAME_4,
)
async def mock_websocket_connection(
hass: HomeAssistant, mock_mozart_client: AsyncMock
) -> None:

View File

@@ -42,6 +42,7 @@ TEST_MODEL_CORE = "Beoconnect Core"
TEST_MODEL_PREMIERE = "Beosound Premiere"
TEST_MODEL_THEATRE = "Beosound Theatre"
TEST_MODEL_LEVEL = "Beosound Level"
TEST_MODEL_A5 = "Beosound A5"
TEST_SERIAL_NUMBER = "11111111"
TEST_NAME = f"{TEST_MODEL_BALANCE}-{TEST_SERIAL_NUMBER}"
TEST_FRIENDLY_NAME = "Living room Balance"
@@ -64,9 +65,11 @@ TEST_JID_3 = f"{TEST_TYPE_NUMBER}.{TEST_ITEM_NUMBER}.{TEST_SERIAL_NUMBER_3}@prod
TEST_MEDIA_PLAYER_ENTITY_ID_3 = f"media_player.beosound_premiere_{TEST_SERIAL_NUMBER_3}"
TEST_HOST_3 = "192.168.0.3"
TEST_FRIENDLY_NAME_4 = "Lounge room Balance"
TEST_JID_4 = f"{TEST_TYPE_NUMBER}.{TEST_ITEM_NUMBER}.44444444@products.bang-olufsen.com"
TEST_MEDIA_PLAYER_ENTITY_ID_4 = "media_player.beosound_balance_44444444"
TEST_FRIENDLY_NAME_4 = "Lounge room A5"
TEST_SERIAL_NUMBER_4 = "44444444"
TEST_NAME_4 = f"{TEST_MODEL_A5}-{TEST_SERIAL_NUMBER_4}"
TEST_JID_4 = f"{TEST_TYPE_NUMBER}.{TEST_ITEM_NUMBER}.{TEST_SERIAL_NUMBER_4}@products.bang-olufsen.com"
TEST_MEDIA_PLAYER_ENTITY_ID_4 = f"media_player.beosound_a5_{TEST_SERIAL_NUMBER_4}"
TEST_HOST_4 = "192.168.0.4"
# Beoremote One
@@ -105,6 +108,13 @@ TEST_DATA_CREATE_ENTRY_3 = {
CONF_NAME: TEST_NAME_3,
}
TEST_DATA_CREATE_ENTRY_4 = {
CONF_HOST: TEST_HOST_4,
CONF_MODEL: TEST_MODEL_A5,
CONF_BEOLINK_JID: TEST_JID_4,
CONF_NAME: TEST_NAME_4,
}
TEST_DATA_ZEROCONF = ZeroconfServiceInfo(
ip_address=IPv4Address(TEST_HOST),
ip_addresses=[IPv4Address(TEST_HOST)],

View File

@@ -45,11 +45,11 @@
'beolink': dict({
'listeners': dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'peers': dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
'self': dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',

View File

@@ -1,4 +1,108 @@
# serializer version: 1
# name: test_button_event_creation_a5
list([
'event.beosound_a5_44444444_bluetooth',
'event.beosound_a5_44444444_next',
'event.beosound_a5_44444444_play_pause',
'event.beosound_a5_44444444_favorite_1',
'event.beosound_a5_44444444_favorite_2',
'event.beosound_a5_44444444_favorite_3',
'event.beosound_a5_44444444_favorite_4',
'event.beosound_a5_44444444_previous',
'event.beosound_a5_44444444_volume',
'event.beoremote_one_55555555_44444444_light_blue',
'event.beoremote_one_55555555_44444444_light_digit_0',
'event.beoremote_one_55555555_44444444_light_digit_1',
'event.beoremote_one_55555555_44444444_light_digit_2',
'event.beoremote_one_55555555_44444444_light_digit_3',
'event.beoremote_one_55555555_44444444_light_digit_4',
'event.beoremote_one_55555555_44444444_light_digit_5',
'event.beoremote_one_55555555_44444444_light_digit_6',
'event.beoremote_one_55555555_44444444_light_digit_7',
'event.beoremote_one_55555555_44444444_light_digit_8',
'event.beoremote_one_55555555_44444444_light_digit_9',
'event.beoremote_one_55555555_44444444_light_down',
'event.beoremote_one_55555555_44444444_light_green',
'event.beoremote_one_55555555_44444444_light_left',
'event.beoremote_one_55555555_44444444_light_play',
'event.beoremote_one_55555555_44444444_light_red',
'event.beoremote_one_55555555_44444444_light_rewind',
'event.beoremote_one_55555555_44444444_light_right',
'event.beoremote_one_55555555_44444444_light_select',
'event.beoremote_one_55555555_44444444_light_stop',
'event.beoremote_one_55555555_44444444_light_up',
'event.beoremote_one_55555555_44444444_light_wind',
'event.beoremote_one_55555555_44444444_light_yellow',
'event.beoremote_one_55555555_44444444_light_function_1',
'event.beoremote_one_55555555_44444444_light_function_2',
'event.beoremote_one_55555555_44444444_light_function_3',
'event.beoremote_one_55555555_44444444_light_function_4',
'event.beoremote_one_55555555_44444444_light_function_5',
'event.beoremote_one_55555555_44444444_light_function_6',
'event.beoremote_one_55555555_44444444_light_function_7',
'event.beoremote_one_55555555_44444444_light_function_8',
'event.beoremote_one_55555555_44444444_light_function_9',
'event.beoremote_one_55555555_44444444_light_function_10',
'event.beoremote_one_55555555_44444444_light_function_11',
'event.beoremote_one_55555555_44444444_light_function_12',
'event.beoremote_one_55555555_44444444_light_function_13',
'event.beoremote_one_55555555_44444444_light_function_14',
'event.beoremote_one_55555555_44444444_light_function_15',
'event.beoremote_one_55555555_44444444_light_function_16',
'event.beoremote_one_55555555_44444444_light_function_17',
'event.beoremote_one_55555555_44444444_control_blue',
'event.beoremote_one_55555555_44444444_control_digit_0',
'event.beoremote_one_55555555_44444444_control_digit_1',
'event.beoremote_one_55555555_44444444_control_digit_2',
'event.beoremote_one_55555555_44444444_control_digit_3',
'event.beoremote_one_55555555_44444444_control_digit_4',
'event.beoremote_one_55555555_44444444_control_digit_5',
'event.beoremote_one_55555555_44444444_control_digit_6',
'event.beoremote_one_55555555_44444444_control_digit_7',
'event.beoremote_one_55555555_44444444_control_digit_8',
'event.beoremote_one_55555555_44444444_control_digit_9',
'event.beoremote_one_55555555_44444444_control_down',
'event.beoremote_one_55555555_44444444_control_green',
'event.beoremote_one_55555555_44444444_control_left',
'event.beoremote_one_55555555_44444444_control_play',
'event.beoremote_one_55555555_44444444_control_red',
'event.beoremote_one_55555555_44444444_control_rewind',
'event.beoremote_one_55555555_44444444_control_right',
'event.beoremote_one_55555555_44444444_control_select',
'event.beoremote_one_55555555_44444444_control_stop',
'event.beoremote_one_55555555_44444444_control_up',
'event.beoremote_one_55555555_44444444_control_wind',
'event.beoremote_one_55555555_44444444_control_yellow',
'event.beoremote_one_55555555_44444444_control_function_1',
'event.beoremote_one_55555555_44444444_control_function_2',
'event.beoremote_one_55555555_44444444_control_function_3',
'event.beoremote_one_55555555_44444444_control_function_4',
'event.beoremote_one_55555555_44444444_control_function_5',
'event.beoremote_one_55555555_44444444_control_function_6',
'event.beoremote_one_55555555_44444444_control_function_7',
'event.beoremote_one_55555555_44444444_control_function_8',
'event.beoremote_one_55555555_44444444_control_function_9',
'event.beoremote_one_55555555_44444444_control_function_10',
'event.beoremote_one_55555555_44444444_control_function_11',
'event.beoremote_one_55555555_44444444_control_function_12',
'event.beoremote_one_55555555_44444444_control_function_13',
'event.beoremote_one_55555555_44444444_control_function_14',
'event.beoremote_one_55555555_44444444_control_function_15',
'event.beoremote_one_55555555_44444444_control_function_16',
'event.beoremote_one_55555555_44444444_control_function_17',
'event.beoremote_one_55555555_44444444_control_function_18',
'event.beoremote_one_55555555_44444444_control_function_19',
'event.beoremote_one_55555555_44444444_control_function_20',
'event.beoremote_one_55555555_44444444_control_function_21',
'event.beoremote_one_55555555_44444444_control_function_22',
'event.beoremote_one_55555555_44444444_control_function_23',
'event.beoremote_one_55555555_44444444_control_function_24',
'event.beoremote_one_55555555_44444444_control_function_25',
'event.beoremote_one_55555555_44444444_control_function_26',
'event.beoremote_one_55555555_44444444_control_function_27',
'media_player.beosound_a5_44444444',
])
# ---
# name: test_button_event_creation_balance
list([
'event.beosound_balance_11111111_bluetooth',
@@ -104,9 +208,8 @@
'media_player.beosound_balance_11111111',
])
# ---
# name: test_button_event_creation_beosound_premiere
# name: test_button_event_creation_premiere
list([
'event.beosound_premiere_33333333_microphone',
'event.beosound_premiere_33333333_next',
'event.beosound_premiere_33333333_play_pause',
'event.beosound_premiere_33333333_favorite_1',
@@ -208,7 +311,7 @@
'media_player.beosound_premiere_33333333',
])
# ---
# name: test_no_button_and_remote_key_event_creation
# name: test_no_button_and_remote_key_event_creation_core
list([
'media_player.beoconnect_core_22222222',
])

View File

@@ -5,11 +5,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -53,11 +53,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -102,11 +102,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -151,11 +151,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -200,11 +200,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -249,11 +249,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -297,11 +297,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -345,11 +345,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -393,11 +393,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -441,11 +441,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -489,11 +489,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -537,11 +537,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -585,11 +585,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -634,11 +634,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com',
@@ -683,11 +683,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -732,11 +732,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com',
@@ -781,11 +781,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -831,11 +831,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com',
@@ -880,11 +880,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -929,11 +929,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com',
@@ -978,11 +978,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -1029,7 +1029,7 @@
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.11111111@products.bang-olufsen.com',
@@ -1072,11 +1072,11 @@
<BeoAttribute.BEOLINK: 'beolink'>: dict({
<BeoAttribute.BEOLINK_LISTENERS: 'listeners'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_PEERS: 'peers'>: dict({
'Bedroom Premiere': '1111.1111111.33333333@products.bang-olufsen.com',
'Lounge room Balance': '1111.1111111.44444444@products.bang-olufsen.com',
'Lounge room A5': '1111.1111111.44444444@products.bang-olufsen.com',
}),
<BeoAttribute.BEOLINK_SELF: 'self'>: dict({
'Living room Balance': '1111.1111111.22222222@products.bang-olufsen.com',

View File

@@ -3,6 +3,7 @@
from unittest.mock import AsyncMock
from mozart_api.models import BeoRemoteButton, ButtonEvent, PairedRemoteResponse
from pytest_unordered import unordered
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.bang_olufsen.const import (
@@ -20,37 +21,64 @@ from .const import (
TEST_BUTTON_EVENT_ENTITY_ID,
TEST_REMOTE_KEY_EVENT_ENTITY_ID,
TEST_SERIAL_NUMBER_3,
TEST_SERIAL_NUMBER_4,
)
from .util import (
get_a5_entity_ids,
get_balance_entity_ids,
get_core_entity_ids,
get_premiere_entity_ids,
get_remote_entity_ids,
)
from .util import get_button_entity_ids, get_remote_entity_ids
from tests.common import MockConfigEntry
async def test_button_event_creation_balance(
async def _check_button_event_creation(
hass: HomeAssistant,
integration: None,
entity_registry: EntityRegistry,
snapshot: SnapshotAssertion,
config_entry: MockConfigEntry,
client: AsyncMock,
entity_ids: list[str],
) -> None:
"""Test button event entities are created when using a Balance (Most devices support all buttons like the Balance)."""
# Add Button Event entity ids
entity_ids: list[str] = [*get_button_entity_ids(), *get_remote_entity_ids()]
"""Test body for entity creation tests."""
# Load entry
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await mock_websocket_connection(hass, client)
# Check that the entities are available
for entity_id in entity_ids:
assert entity_registry.async_get(entity_id)
# Check number of entities
# The media_player entity and all of the button event entities should be the only available
# Check that no entities other than the expected have been created
entity_ids_available = list(entity_registry.entities.keys())
assert len(entity_ids_available) == 1 + len(entity_ids)
# Check snapshot
assert entity_ids_available == unordered(entity_ids)
assert entity_ids_available == snapshot
async def test_no_button_and_remote_key_event_creation(
async def test_button_event_creation_balance(
hass: HomeAssistant,
entity_registry: EntityRegistry,
snapshot: SnapshotAssertion,
mock_config_entry: MockConfigEntry,
mock_mozart_client: AsyncMock,
) -> None:
"""Test button event entities are created when using a Balance (Most devices support all buttons like the Balance)."""
await _check_button_event_creation(
hass,
entity_registry,
snapshot,
mock_config_entry,
mock_mozart_client,
[*get_balance_entity_ids(), *get_remote_entity_ids()],
)
async def test_no_button_and_remote_key_event_creation_core(
hass: HomeAssistant,
mock_config_entry_core: MockConfigEntry,
mock_mozart_client: AsyncMock,
@@ -62,51 +90,58 @@ async def test_no_button_and_remote_key_event_creation(
items=[]
)
# Load entry
mock_config_entry_core.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry_core.entry_id)
await mock_websocket_connection(hass, mock_mozart_client)
# Check number of entities
# The media_player entity should be the only available
entity_ids_available = list(entity_registry.entities.keys())
assert len(entity_ids_available) == 1
# Check snapshot
assert entity_ids_available == snapshot
await _check_button_event_creation(
hass,
entity_registry,
snapshot,
mock_config_entry_core,
mock_mozart_client,
get_core_entity_ids(),
)
async def test_button_event_creation_beosound_premiere(
async def test_button_event_creation_premiere(
hass: HomeAssistant,
mock_config_entry_premiere: MockConfigEntry,
mock_mozart_client: AsyncMock,
entity_registry: EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test Bluetooth button event entity is not created when using a Beosound Premiere."""
"""Test Bluetooth and Microphone button event entities are not created when using a Beosound Premiere."""
# Load entry
mock_config_entry_premiere.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry_premiere.entry_id)
await mock_websocket_connection(hass, mock_mozart_client)
await _check_button_event_creation(
hass,
entity_registry,
snapshot,
mock_config_entry_premiere,
mock_mozart_client,
[
*get_premiere_entity_ids(),
*get_remote_entity_ids(device_serial=TEST_SERIAL_NUMBER_3),
],
)
# Add Button Event entity ids
entity_ids = [
*get_button_entity_ids("beosound_premiere_33333333"),
*get_remote_entity_ids(device_serial=TEST_SERIAL_NUMBER_3),
]
entity_ids.remove("event.beosound_premiere_33333333_bluetooth")
# Check that the entities are available
for entity_id in entity_ids:
assert entity_registry.async_get(entity_id)
async def test_button_event_creation_a5(
hass: HomeAssistant,
mock_config_entry_a5: MockConfigEntry,
mock_mozart_client: AsyncMock,
entity_registry: EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test Microphone button event entity is not created when using a Beosound A5."""
# Check number of entities
# The media_player entity and all of the button event entities (except Bluetooth) should be the only available
entity_ids_available = list(entity_registry.entities.keys())
assert len(entity_ids_available) == 1 + len(entity_ids)
assert entity_ids_available == snapshot
await _check_button_event_creation(
hass,
entity_registry,
snapshot,
mock_config_entry_a5,
mock_mozart_client,
[
*get_a5_entity_ids(),
*get_remote_entity_ids(device_serial=TEST_SERIAL_NUMBER_4),
],
)
async def test_button(

View File

@@ -10,6 +10,7 @@ from mozart_api.models import (
WebsocketNotificationTag,
)
import pytest
from pytest_unordered import unordered
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.bang_olufsen.const import (
@@ -29,7 +30,7 @@ from .const import (
TEST_REMOTE_SERIAL_PAIRED,
TEST_SERIAL_NUMBER,
)
from .util import get_button_entity_ids, get_remote_entity_ids
from .util import get_balance_entity_ids, get_remote_entity_ids
from tests.common import MockConfigEntry
@@ -133,9 +134,8 @@ async def test_on_remote_control_already_added(
assert device_registry.async_get_device({(DOMAIN, TEST_REMOTE_SERIAL_PAIRED)})
# Check number of entities (remote and button events and media_player)
assert (
len(list(entity_registry.entities.keys()))
== len(get_remote_entity_ids()) + len(get_button_entity_ids()) + 1
assert list(entity_registry.entities.keys()) == unordered(
[*get_balance_entity_ids(), *get_remote_entity_ids()]
)
remote_callback = mock_mozart_client.get_notification_notifications.call_args[0][0]
@@ -152,12 +152,11 @@ async def test_on_remote_control_already_added(
assert mock_mozart_client.get_bluetooth_remotes.call_count == 2
assert device_registry.async_get_device({(DOMAIN, TEST_REMOTE_SERIAL_PAIRED)})
# Check number of entities
# Check number of entities (remote and button events and media_player)
entity_ids_available = list(entity_registry.entities.keys())
assert (
len(entity_ids_available)
== len(get_remote_entity_ids()) + len(get_button_entity_ids()) + 1
assert list(entity_registry.entities.keys()) == unordered(
[*get_balance_entity_ids(), *get_remote_entity_ids()]
)
assert entity_ids_available == snapshot
@@ -180,10 +179,9 @@ async def test_on_remote_control_paired(
assert mock_mozart_client.get_bluetooth_remotes.call_count == 1
assert device_registry.async_get_device({(DOMAIN, TEST_REMOTE_SERIAL_PAIRED)})
# Check number of entities (button events and media_player)
assert (
len(list(entity_registry.entities.keys()))
== len(get_remote_entity_ids()) + len(get_button_entity_ids()) + 1
# Check number of entities (button and remote events and media_player)
assert list(entity_registry.entities.keys()) == unordered(
[*get_balance_entity_ids(), *get_remote_entity_ids()]
)
# "Pair" a new remote
mock_mozart_client.get_bluetooth_remotes.return_value = PairedRemoteResponse(
@@ -234,12 +232,12 @@ async def test_on_remote_control_paired(
# Check number of entities (remote and button events and media_player)
entity_ids_available = list(entity_registry.entities.keys())
assert (
len(entity_ids_available)
== len(get_remote_entity_ids())
+ len(get_remote_entity_ids())
+ len(get_button_entity_ids())
+ 1
assert entity_ids_available == unordered(
[
*get_balance_entity_ids(),
*get_remote_entity_ids(),
*get_remote_entity_ids("66666666"),
]
)
assert entity_ids_available == snapshot
@@ -262,11 +260,11 @@ async def test_on_remote_control_unpaired(
assert mock_mozart_client.get_bluetooth_remotes.call_count == 1
assert device_registry.async_get_device({(DOMAIN, TEST_REMOTE_SERIAL_PAIRED)})
# Check number of entities (button events and media_player)
assert (
len(list(entity_registry.entities.keys()))
== len(get_remote_entity_ids()) + len(get_button_entity_ids()) + 1
# Check number of entities (button and remote events and media_player)
assert list(entity_registry.entities.keys()) == unordered(
[*get_balance_entity_ids(), *get_remote_entity_ids()]
)
# "Unpair" the remote
mock_mozart_client.get_bluetooth_remotes.return_value = PairedRemoteResponse(
items=[]
@@ -296,7 +294,7 @@ async def test_on_remote_control_unpaired(
# Check number of entities (button events and media_player)
entity_ids_available = list(entity_registry.entities.keys())
assert len(entity_ids_available) == +len(get_button_entity_ids()) + 1
assert entity_ids_available == unordered(get_balance_entity_ids())
assert entity_ids_available == snapshot

View File

@@ -10,17 +10,58 @@ from homeassistant.components.bang_olufsen.const import (
DEVICE_BUTTONS,
)
from .const import TEST_REMOTE_SERIAL, TEST_SERIAL_NUMBER
from .const import (
TEST_MEDIA_PLAYER_ENTITY_ID,
TEST_MEDIA_PLAYER_ENTITY_ID_2,
TEST_MEDIA_PLAYER_ENTITY_ID_3,
TEST_MEDIA_PLAYER_ENTITY_ID_4,
TEST_REMOTE_SERIAL,
TEST_SERIAL_NUMBER,
)
def get_button_entity_ids(id_prefix: str = "beosound_balance_11111111") -> list[str]:
"""Return a list of button entity_ids that Mozart devices (except Beoconnect Core and Beosound Premiere) provides."""
def _get_button_entity_ids(id_prefix: str = "beosound_balance_11111111") -> list[str]:
"""Return a list of button entity_ids that Mozart devices provide.
Beoconnect Core, Beosound A5, Beosound A9 and Beosound Premiere do not have (all of the) physical buttons and need filtering.
"""
return [
f"event.{id_prefix}_{underscore(button_type)}".replace("preset", "favorite_")
for button_type in DEVICE_BUTTONS
]
def get_balance_entity_ids() -> list[str]:
"""Return a list of entity_ids that a Beosound Balance provides."""
return [TEST_MEDIA_PLAYER_ENTITY_ID, *_get_button_entity_ids()]
def get_premiere_entity_ids() -> list[str]:
"""Return a list of entity_ids that a Beosound Premiere provides."""
buttons = [
TEST_MEDIA_PLAYER_ENTITY_ID_3,
*_get_button_entity_ids("beosound_premiere_33333333"),
]
buttons.remove("event.beosound_premiere_33333333_bluetooth")
buttons.remove("event.beosound_premiere_33333333_microphone")
return buttons
def get_a5_entity_ids() -> list[str]:
"""Return a list of entity_ids that a Beosound A5 provides."""
buttons = [
TEST_MEDIA_PLAYER_ENTITY_ID_4,
*_get_button_entity_ids("beosound_a5_44444444"),
]
buttons.remove("event.beosound_a5_44444444_microphone")
return buttons
def get_core_entity_ids() -> list[str]:
"""Return a list of entity_ids that a Beoconnect core provides."""
return [TEST_MEDIA_PLAYER_ENTITY_ID_2]
def get_remote_entity_ids(
remote_serial: str = TEST_REMOTE_SERIAL, device_serial: str = TEST_SERIAL_NUMBER
) -> list[str]:

View File

@@ -82,6 +82,10 @@ def patch_doorbird_api_entry_points(api: MagicMock) -> Generator[DoorBird]:
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=api,
),
patch(
"homeassistant.components.doorbird.device.get_url",
return_value="http://127.0.0.1:8123",
),
):
yield api

View File

@@ -2,15 +2,141 @@
from copy import deepcopy
from http import HTTPStatus
from typing import Any
from doorbirdpy import DoorBirdScheduleEntry
import pytest
from homeassistant.components.doorbird.const import CONF_EVENTS
from homeassistant.components.doorbird.const import (
CONF_EVENTS,
DEFAULT_DOORBELL_EVENT,
DEFAULT_MOTION_EVENT,
DOMAIN,
)
from homeassistant.core import HomeAssistant
from . import VALID_CONFIG
from .conftest import DoorbirdMockerType
from tests.common import MockConfigEntry
@pytest.fixture
def doorbird_favorites_with_stale() -> dict[str, dict[str, Any]]:
"""Return favorites fixture with stale favorites from another HA instance.
Creates favorites where identifier "2" has the same event name as "0"
(mydoorbird_doorbell) but points to a different HA instance URL.
These stale favorites should be filtered out.
"""
return {
"http": {
"0": {
"title": "Home Assistant (mydoorbird_doorbell)",
"value": "http://127.0.0.1:8123/api/doorbird/mydoorbird_doorbell?token=test-token",
},
# Stale favorite from a different HA instance - should be filtered out
"2": {
"title": "Home Assistant (mydoorbird_doorbell)",
"value": "http://old-ha-instance:8123/api/doorbird/mydoorbird_doorbell?token=old-token",
},
"5": {
"title": "Home Assistant (mydoorbird_motion)",
"value": "http://127.0.0.1:8123/api/doorbird/mydoorbird_motion?token=test-token",
},
}
}
@pytest.fixture
def doorbird_schedule_with_stale() -> list[DoorBirdScheduleEntry]:
"""Return schedule fixture with outputs referencing stale favorites.
Both param "0" and "2" map to doorbell input, but "2" is a stale favorite.
"""
schedule_data = [
{
"input": "doorbell",
"param": "1",
"output": [
{
"event": "http",
"param": "0",
"schedule": {"weekdays": [{"to": "107999", "from": "108000"}]},
},
{
"event": "http",
"param": "2",
"schedule": {"weekdays": [{"to": "107999", "from": "108000"}]},
},
],
},
{
"input": "motion",
"param": "",
"output": [
{
"event": "http",
"param": "5",
"schedule": {"weekdays": [{"to": "107999", "from": "108000"}]},
},
],
},
]
return DoorBirdScheduleEntry.parse_all(schedule_data)
async def test_stale_favorites_filtered_by_url(
hass: HomeAssistant,
doorbird_mocker: DoorbirdMockerType,
doorbird_favorites_with_stale: dict[str, dict[str, Any]],
doorbird_schedule_with_stale: list[DoorBirdScheduleEntry],
) -> None:
"""Test that stale favorites from other HA instances are filtered out."""
await doorbird_mocker(
favorites=doorbird_favorites_with_stale,
schedule=doorbird_schedule_with_stale,
)
# Should have 2 event entities - stale favorite "2" is filtered out
# because its URL doesn't match the current HA instance
event_entities = hass.states.async_all("event")
assert len(event_entities) == 2
async def test_custom_url_used_for_favorites(
hass: HomeAssistant,
doorbird_mocker: DoorbirdMockerType,
) -> None:
"""Test that custom URL override is used instead of get_url."""
custom_url = "https://my-custom-url.example.com:8443"
favorites = {
"http": {
"1": {
"title": "Home Assistant (mydoorbird_doorbell)",
"value": f"{custom_url}/api/doorbird/mydoorbird_doorbell?token=test-token",
},
"2": {
"title": "Home Assistant (mydoorbird_motion)",
"value": f"{custom_url}/api/doorbird/mydoorbird_motion?token=test-token",
},
}
}
config_with_custom_url = {
**VALID_CONFIG,
"hass_url_override": custom_url,
}
entry = MockConfigEntry(
domain=DOMAIN,
unique_id="1CCAE3AAAAAA",
data=config_with_custom_url,
options={CONF_EVENTS: [DEFAULT_DOORBELL_EVENT, DEFAULT_MOTION_EVENT]},
)
await doorbird_mocker(entry=entry, favorites=favorites)
# Should have 2 event entities using the custom URL
event_entities = hass.states.async_all("event")
assert len(event_entities) == 2
async def test_no_configured_events(
hass: HomeAssistant,

View File

@@ -2345,19 +2345,20 @@ async def test_effect_template(
],
)
@pytest.mark.parametrize(
("expected_min_mireds", "attribute_template"),
("expected_min_mireds", "expected_max_kelvin", "attribute_template"),
[
(118, "{{118}}"),
(153, "{{x - 12}}"),
(153, "None"),
(153, "{{ none }}"),
(153, ""),
(153, "{{ 'a' }}"),
(118, 8474, "{{118}}"),
(153, 6535, "{{x - 12}}"),
(153, 6535, "None"),
(153, 6535, "{{ none }}"),
(153, 6535, ""),
(153, 6535, "{{ 'a' }}"),
],
)
async def test_min_mireds_template(
hass: HomeAssistant,
expected_min_mireds,
expected_min_mireds: int,
expected_max_kelvin: int,
style: ConfigurationStyle,
setup_light_with_mireds,
) -> None:
@@ -2369,6 +2370,7 @@ async def test_min_mireds_template(
state = hass.states.get("light.test_template_light")
assert state is not None
assert state.attributes.get("min_mireds") == expected_min_mireds
assert state.attributes.get("max_color_temp_kelvin") == expected_max_kelvin
@pytest.mark.parametrize("count", [1])
@@ -2381,19 +2383,20 @@ async def test_min_mireds_template(
],
)
@pytest.mark.parametrize(
("expected_max_mireds", "attribute_template"),
("expected_max_mireds", "expected_min_kelvin", "attribute_template"),
[
(488, "{{488}}"),
(500, "{{x - 12}}"),
(500, "None"),
(500, "{{ none }}"),
(500, ""),
(500, "{{ 'a' }}"),
(488, 2049, "{{488}}"),
(500, 2000, "{{x - 12}}"),
(500, 2000, "None"),
(500, 2000, "{{ none }}"),
(500, 2000, ""),
(500, 2000, "{{ 'a' }}"),
],
)
async def test_max_mireds_template(
hass: HomeAssistant,
expected_max_mireds,
expected_max_mireds: int,
expected_min_kelvin: int,
style: ConfigurationStyle,
setup_light_with_mireds,
) -> None:
@@ -2405,6 +2408,7 @@ async def test_max_mireds_template(
state = hass.states.get("light.test_template_light")
assert state is not None
assert state.attributes.get("max_mireds") == expected_max_mireds
assert state.attributes.get("min_color_temp_kelvin") == expected_min_kelvin
@pytest.mark.parametrize(

View File

@@ -15,7 +15,7 @@
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'light.device_with_led_led',
'has_entity_name': True,
'hidden_by': None,

View File

@@ -37,7 +37,6 @@ from homeassistant.helpers.automation import move_top_level_schema_fields_to_opt
from homeassistant.helpers.condition import (
Condition,
ConditionCheckerType,
ConditionConfig,
async_validate_condition_config,
)
from homeassistant.helpers.template import Template
@@ -2124,9 +2123,6 @@ async def test_platform_multiple_conditions(hass: HomeAssistant) -> None:
"""Validate config."""
return config
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
"""Initialize condition."""
class MockCondition1(MockCondition):
"""Mock condition 1."""