Merge pull request #76964 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2022-08-18 08:09:01 -04:00 committed by GitHub
commit c894ddeb95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 739 additions and 87 deletions

View File

@ -16,6 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .base import AcmedaBase from .base import AcmedaBase
from .const import ACMEDA_HUB_UPDATE, DOMAIN from .const import ACMEDA_HUB_UPDATE, DOMAIN
from .helpers import async_add_acmeda_entities from .helpers import async_add_acmeda_entities
from .hub import PulseHub
async def async_setup_entry( async def async_setup_entry(
@ -24,7 +25,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Acmeda Rollers from a config entry.""" """Set up the Acmeda Rollers from a config entry."""
hub = hass.data[DOMAIN][config_entry.entry_id] hub: PulseHub = hass.data[DOMAIN][config_entry.entry_id]
current: set[int] = set() current: set[int] = set()
@ -122,6 +123,6 @@ class AcmedaCover(AcmedaBase, CoverEntity):
"""Stop the roller.""" """Stop the roller."""
await self.roller.move_stop() await self.roller.move_stop()
async def async_set_cover_tilt(self, **kwargs): async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Tilt the roller shutter to a specific position.""" """Tilt the roller shutter to a specific position."""
await self.roller.move_to(100 - kwargs[ATTR_POSITION]) await self.roller.move_to(100 - kwargs[ATTR_POSITION])

View File

@ -15,13 +15,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import LENGTH, PERCENTAGE, VOLUME
LENGTH_KILOMETERS,
LENGTH_MILES,
PERCENTAGE,
VOLUME_GALLONS,
VOLUME_LITERS,
)
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
@ -39,8 +33,7 @@ class BMWSensorEntityDescription(SensorEntityDescription):
"""Describes BMW sensor entity.""" """Describes BMW sensor entity."""
key_class: str | None = None key_class: str | None = None
unit_metric: str | None = None unit_type: str | None = None
unit_imperial: str | None = None
value: Callable = lambda x, y: x value: Callable = lambda x, y: x
@ -86,56 +79,49 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = {
"remaining_battery_percent": BMWSensorEntityDescription( "remaining_battery_percent": BMWSensorEntityDescription(
key="remaining_battery_percent", key="remaining_battery_percent",
key_class="fuel_and_battery", key_class="fuel_and_battery",
unit_metric=PERCENTAGE, unit_type=PERCENTAGE,
unit_imperial=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY, device_class=SensorDeviceClass.BATTERY,
), ),
# --- Specific --- # --- Specific ---
"mileage": BMWSensorEntityDescription( "mileage": BMWSensorEntityDescription(
key="mileage", key="mileage",
icon="mdi:speedometer", icon="mdi:speedometer",
unit_metric=LENGTH_KILOMETERS, unit_type=LENGTH,
unit_imperial=LENGTH_MILES,
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
), ),
"remaining_range_total": BMWSensorEntityDescription( "remaining_range_total": BMWSensorEntityDescription(
key="remaining_range_total", key="remaining_range_total",
key_class="fuel_and_battery", key_class="fuel_and_battery",
icon="mdi:map-marker-distance", icon="mdi:map-marker-distance",
unit_metric=LENGTH_KILOMETERS, unit_type=LENGTH,
unit_imperial=LENGTH_MILES,
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
), ),
"remaining_range_electric": BMWSensorEntityDescription( "remaining_range_electric": BMWSensorEntityDescription(
key="remaining_range_electric", key="remaining_range_electric",
key_class="fuel_and_battery", key_class="fuel_and_battery",
icon="mdi:map-marker-distance", icon="mdi:map-marker-distance",
unit_metric=LENGTH_KILOMETERS, unit_type=LENGTH,
unit_imperial=LENGTH_MILES,
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
), ),
"remaining_range_fuel": BMWSensorEntityDescription( "remaining_range_fuel": BMWSensorEntityDescription(
key="remaining_range_fuel", key="remaining_range_fuel",
key_class="fuel_and_battery", key_class="fuel_and_battery",
icon="mdi:map-marker-distance", icon="mdi:map-marker-distance",
unit_metric=LENGTH_KILOMETERS, unit_type=LENGTH,
unit_imperial=LENGTH_MILES,
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
), ),
"remaining_fuel": BMWSensorEntityDescription( "remaining_fuel": BMWSensorEntityDescription(
key="remaining_fuel", key="remaining_fuel",
key_class="fuel_and_battery", key_class="fuel_and_battery",
icon="mdi:gas-station", icon="mdi:gas-station",
unit_metric=VOLUME_LITERS, unit_type=VOLUME,
unit_imperial=VOLUME_GALLONS,
value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2), value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2),
), ),
"remaining_fuel_percent": BMWSensorEntityDescription( "remaining_fuel_percent": BMWSensorEntityDescription(
key="remaining_fuel_percent", key="remaining_fuel_percent",
key_class="fuel_and_battery", key_class="fuel_and_battery",
icon="mdi:gas-station", icon="mdi:gas-station",
unit_metric=PERCENTAGE, unit_type=PERCENTAGE,
unit_imperial=PERCENTAGE,
), ),
} }
@ -182,8 +168,12 @@ class BMWSensor(BMWBaseEntity, SensorEntity):
self._attr_name = f"{vehicle.name} {description.key}" self._attr_name = f"{vehicle.name} {description.key}"
self._attr_unique_id = f"{vehicle.vin}-{description.key}" self._attr_unique_id = f"{vehicle.vin}-{description.key}"
# Force metric system as BMW API apparently only returns metric values now # Set the correct unit of measurement based on the unit_type
self._attr_native_unit_of_measurement = description.unit_metric if description.unit_type:
self._attr_native_unit_of_measurement = (
coordinator.hass.config.units.as_dict().get(description.unit_type)
or description.unit_type
)
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:

View File

@ -7,12 +7,20 @@ from homeassistant.const import CONF_API_TOKEN, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN from .const import DATA_HASS_CONFIG, DOMAIN
PLATFORMS = [Platform.NOTIFY] PLATFORMS = [Platform.NOTIFY]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Discord component."""
hass.data[DATA_HASS_CONFIG] = config
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Discord from a config entry.""" """Set up Discord from a config entry."""
nextcord.VoiceClient.warn_nacl = False nextcord.VoiceClient.warn_nacl = False
@ -30,11 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.async_create_task( hass.async_create_task(
discovery.async_load_platform( discovery.async_load_platform(
hass, hass, Platform.NOTIFY, DOMAIN, dict(entry.data), hass.data[DATA_HASS_CONFIG]
Platform.NOTIFY,
DOMAIN,
hass.data[DOMAIN][entry.entry_id],
hass.data[DOMAIN],
) )
) )

View File

@ -8,3 +8,5 @@ DEFAULT_NAME = "Discord"
DOMAIN: Final = "discord" DOMAIN: Final = "discord"
URL_PLACEHOLDER = {CONF_URL: "https://www.home-assistant.io/integrations/discord"} URL_PLACEHOLDER = {CONF_URL: "https://www.home-assistant.io/integrations/discord"}
DATA_HASS_CONFIG = "discord_hass_config"

View File

@ -75,9 +75,8 @@ class MobileAppBinarySensor(MobileAppEntity, BinarySensorEntity):
"""Return the state of the binary sensor.""" """Return the state of the binary sensor."""
return self._config[ATTR_SENSOR_STATE] return self._config[ATTR_SENSOR_STATE]
@callback async def async_restore_last_state(self, last_state):
def async_restore_last_state(self, last_state):
"""Restore previous state.""" """Restore previous state."""
super().async_restore_last_state(last_state) await super().async_restore_last_state(last_state)
self._config[ATTR_SENSOR_STATE] = last_state.state == STATE_ON self._config[ATTR_SENSOR_STATE] = last_state.state == STATE_ON

View File

@ -43,10 +43,9 @@ class MobileAppEntity(RestoreEntity):
if (state := await self.async_get_last_state()) is None: if (state := await self.async_get_last_state()) is None:
return return
self.async_restore_last_state(state) await self.async_restore_last_state(state)
@callback async def async_restore_last_state(self, last_state):
def async_restore_last_state(self, last_state):
"""Restore previous state.""" """Restore previous state."""
self._config[ATTR_SENSOR_STATE] = last_state.state self._config[ATTR_SENSOR_STATE] = last_state.state
self._config[ATTR_SENSOR_ATTRIBUTES] = { self._config[ATTR_SENSOR_ATTRIBUTES] = {

View File

@ -3,9 +3,9 @@ from __future__ import annotations
from typing import Any from typing import Any
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_WEBHOOK_ID, STATE_UNKNOWN from homeassistant.const import CONF_WEBHOOK_ID, STATE_UNKNOWN, TEMP_CELSIUS
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -27,6 +27,7 @@ from .const import (
DOMAIN, DOMAIN,
) )
from .entity import MobileAppEntity from .entity import MobileAppEntity
from .webhook import _extract_sensor_unique_id
async def async_setup_entry( async def async_setup_entry(
@ -73,9 +74,30 @@ async def async_setup_entry(
) )
class MobileAppSensor(MobileAppEntity, SensorEntity): class MobileAppSensor(MobileAppEntity, RestoreSensor):
"""Representation of an mobile app sensor.""" """Representation of an mobile app sensor."""
async def async_restore_last_state(self, last_state):
"""Restore previous state."""
await super().async_restore_last_state(last_state)
if not (last_sensor_data := await self.async_get_last_sensor_data()):
# Workaround to handle migration to RestoreSensor, can be removed
# in HA Core 2023.4
self._config[ATTR_SENSOR_STATE] = None
webhook_id = self._entry.data[CONF_WEBHOOK_ID]
sensor_unique_id = _extract_sensor_unique_id(webhook_id, self.unique_id)
if (
self.device_class == SensorDeviceClass.TEMPERATURE
and sensor_unique_id == "battery_temperature"
):
self._config[ATTR_SENSOR_UOM] = TEMP_CELSIUS
return
self._config[ATTR_SENSOR_STATE] = last_sensor_data.native_value
self._config[ATTR_SENSOR_UOM] = last_sensor_data.native_unit_of_measurement
@property @property
def native_value(self): def native_value(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""

View File

@ -2,7 +2,7 @@
"domain": "netgear", "domain": "netgear",
"name": "NETGEAR", "name": "NETGEAR",
"documentation": "https://www.home-assistant.io/integrations/netgear", "documentation": "https://www.home-assistant.io/integrations/netgear",
"requirements": ["pynetgear==0.10.6"], "requirements": ["pynetgear==0.10.7"],
"codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"],
"iot_class": "local_polling", "iot_class": "local_polling",
"config_flow": true, "config_flow": true,

View File

@ -1,6 +1,8 @@
"""Provides functionality to notify people.""" """Provides functionality to notify people."""
from __future__ import annotations from __future__ import annotations
import asyncio
import voluptuous as vol import voluptuous as vol
import homeassistant.components.persistent_notification as pn import homeassistant.components.persistent_notification as pn
@ -40,13 +42,19 @@ PLATFORM_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the notify services.""" """Set up the notify services."""
platform_setups = async_setup_legacy(hass, config)
# We need to add the component here break the deadlock # We need to add the component here break the deadlock
# when setting up integrations from config entries as # when setting up integrations from config entries as
# they would otherwise wait for notify to be # they would otherwise wait for notify to be
# setup and thus the config entries would not be able to # setup and thus the config entries would not be able to
# setup their platforms. # setup their platforms, but we need to do it after
# the dispatcher is connected so we don't miss integrations
# that are registered before the dispatcher is connected
hass.config.components.add(DOMAIN) hass.config.components.add(DOMAIN)
await async_setup_legacy(hass, config)
if platform_setups:
await asyncio.wait([asyncio.create_task(setup) for setup in platform_setups])
async def persistent_notification(service: ServiceCall) -> None: async def persistent_notification(service: ServiceCall) -> None:
"""Send notification via the built-in persistsent_notify integration.""" """Send notification via the built-in persistsent_notify integration."""

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Coroutine
from functools import partial from functools import partial
from typing import Any, cast from typing import Any, cast
@ -32,7 +33,10 @@ NOTIFY_SERVICES = "notify_services"
NOTIFY_DISCOVERY_DISPATCHER = "notify_discovery_dispatcher" NOTIFY_DISCOVERY_DISPATCHER = "notify_discovery_dispatcher"
async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None: @callback
def async_setup_legacy(
hass: HomeAssistant, config: ConfigType
) -> list[Coroutine[Any, Any, None]]:
"""Set up legacy notify services.""" """Set up legacy notify services."""
hass.data.setdefault(NOTIFY_SERVICES, {}) hass.data.setdefault(NOTIFY_SERVICES, {})
hass.data.setdefault(NOTIFY_DISCOVERY_DISPATCHER, None) hass.data.setdefault(NOTIFY_DISCOVERY_DISPATCHER, None)
@ -101,15 +105,6 @@ async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None:
) )
hass.config.components.add(f"{DOMAIN}.{integration_name}") hass.config.components.add(f"{DOMAIN}.{integration_name}")
setup_tasks = [
asyncio.create_task(async_setup_platform(integration_name, p_config))
for integration_name, p_config in config_per_platform(config, DOMAIN)
if integration_name is not None
]
if setup_tasks:
await asyncio.wait(setup_tasks)
async def async_platform_discovered( async def async_platform_discovered(
platform: str, info: DiscoveryInfoType | None platform: str, info: DiscoveryInfoType | None
) -> None: ) -> None:
@ -120,6 +115,12 @@ async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None:
hass, DOMAIN, async_platform_discovered hass, DOMAIN, async_platform_discovered
) )
return [
async_setup_platform(integration_name, p_config)
for integration_name, p_config in config_per_platform(config, DOMAIN)
if integration_name is not None
]
@callback @callback
def check_templates_warn(hass: HomeAssistant, tpl: template.Template) -> None: def check_templates_warn(hass: HomeAssistant, tpl: template.Template) -> None:

View File

@ -111,8 +111,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) or OVERKIZ_DEVICE_TO_PLATFORM.get(device.ui_class): ) or OVERKIZ_DEVICE_TO_PLATFORM.get(device.ui_class):
platforms[platform].append(device) platforms[platform].append(device)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
for gateway in setup.gateways: for gateway in setup.gateways:
@ -128,6 +126,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
configuration_url=server.configuration_url, configuration_url=server.configuration_url,
) )
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True

View File

@ -12,7 +12,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, discovery from homeassistant.helpers import aiohttp_client, discovery
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import DATA_CLIENT, DOMAIN from .const import DATA_CLIENT, DATA_HASS_CONFIG, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -21,6 +21,8 @@ PLATFORMS = [Platform.NOTIFY]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Slack component.""" """Set up the Slack component."""
hass.data[DATA_HASS_CONFIG] = config
# Iterate all entries for notify to only get Slack # Iterate all entries for notify to only get Slack
if Platform.NOTIFY in config: if Platform.NOTIFY in config:
for entry in config[Platform.NOTIFY]: for entry in config[Platform.NOTIFY]:
@ -55,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Platform.NOTIFY, Platform.NOTIFY,
DOMAIN, DOMAIN,
hass.data[DOMAIN][entry.entry_id], hass.data[DOMAIN][entry.entry_id],
hass.data[DOMAIN], hass.data[DATA_HASS_CONFIG],
) )
) )

View File

@ -14,3 +14,5 @@ CONF_DEFAULT_CHANNEL = "default_channel"
DATA_CLIENT = "client" DATA_CLIENT = "client"
DEFAULT_TIMEOUT_SECONDS = 15 DEFAULT_TIMEOUT_SECONDS = 15
DOMAIN: Final = "slack" DOMAIN: Final = "slack"
DATA_HASS_CONFIG = "slack_hass_config"

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 8 MINOR_VERSION: Final = 8
PATCH_VERSION: Final = "5" PATCH_VERSION: Final = "6"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2022.8.5" version = "2022.8.6"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"

View File

@ -1683,7 +1683,7 @@ pymyq==3.1.4
pymysensors==0.22.1 pymysensors==0.22.1
# homeassistant.components.netgear # homeassistant.components.netgear
pynetgear==0.10.6 pynetgear==0.10.7
# homeassistant.components.netio # homeassistant.components.netio
pynetio==0.1.9.1 pynetio==0.1.9.1

View File

@ -1160,7 +1160,7 @@ pymyq==3.1.4
pymysensors==0.22.1 pymysensors==0.22.1
# homeassistant.components.netgear # homeassistant.components.netgear
pynetgear==0.10.6 pynetgear==0.10.7
# homeassistant.components.nina # homeassistant.components.nina
pynina==0.1.8 pynina==0.1.8

View File

@ -1,17 +1,29 @@
"""Tests for the for the BMW Connected Drive integration.""" """Tests for the for the BMW Connected Drive integration."""
import json
from pathlib import Path
from bimmer_connected.account import MyBMWAccount
from bimmer_connected.api.utils import log_to_to_file
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.bmw_connected_drive.const import ( from homeassistant.components.bmw_connected_drive.const import (
CONF_READ_ONLY, CONF_READ_ONLY,
CONF_REFRESH_TOKEN,
DOMAIN as BMW_DOMAIN, DOMAIN as BMW_DOMAIN,
) )
from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.util.dt import utcnow
from tests.common import MockConfigEntry, get_fixture_path, load_fixture
FIXTURE_USER_INPUT = { FIXTURE_USER_INPUT = {
CONF_USERNAME: "user@domain.com", CONF_USERNAME: "user@domain.com",
CONF_PASSWORD: "p4ssw0rd", CONF_PASSWORD: "p4ssw0rd",
CONF_REGION: "rest_of_world", CONF_REGION: "rest_of_world",
} }
FIXTURE_REFRESH_TOKEN = "SOME_REFRESH_TOKEN"
FIXTURE_CONFIG_ENTRY = { FIXTURE_CONFIG_ENTRY = {
"entry_id": "1", "entry_id": "1",
@ -21,8 +33,82 @@ FIXTURE_CONFIG_ENTRY = {
CONF_USERNAME: FIXTURE_USER_INPUT[CONF_USERNAME], CONF_USERNAME: FIXTURE_USER_INPUT[CONF_USERNAME],
CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD], CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD],
CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION], CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION],
CONF_REFRESH_TOKEN: FIXTURE_REFRESH_TOKEN,
}, },
"options": {CONF_READ_ONLY: False}, "options": {CONF_READ_ONLY: False},
"source": config_entries.SOURCE_USER, "source": config_entries.SOURCE_USER,
"unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}",
} }
async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None:
"""Load MyBMWVehicle from fixtures and add them to the account."""
fixture_path = Path(get_fixture_path("", integration=BMW_DOMAIN))
fixture_vehicles_bmw = list(fixture_path.rglob("vehicles_v2_bmw_*.json"))
fixture_vehicles_mini = list(fixture_path.rglob("vehicles_v2_mini_*.json"))
# Load vehicle base lists as provided by vehicles/v2 API
vehicles = {
"bmw": [
vehicle
for bmw_file in fixture_vehicles_bmw
for vehicle in json.loads(load_fixture(bmw_file, integration=BMW_DOMAIN))
],
"mini": [
vehicle
for mini_file in fixture_vehicles_mini
for vehicle in json.loads(load_fixture(mini_file, integration=BMW_DOMAIN))
],
}
fetched_at = utcnow()
# simulate storing fingerprints
if account.config.log_response_path:
for brand in ["bmw", "mini"]:
log_to_to_file(
json.dumps(vehicles[brand]),
account.config.log_response_path,
f"vehicles_v2_{brand}",
)
# Create a vehicle with base + specific state as provided by state/VIN API
for vehicle_base in [vehicle for brand in vehicles.values() for vehicle in brand]:
vehicle_state_path = (
Path("vehicles")
/ vehicle_base["attributes"]["bodyType"]
/ f"state_{vehicle_base['vin']}_0.json"
)
vehicle_state = json.loads(
load_fixture(
vehicle_state_path,
integration=BMW_DOMAIN,
)
)
account.add_vehicle(
vehicle_base,
vehicle_state,
fetched_at,
)
# simulate storing fingerprints
if account.config.log_response_path:
log_to_to_file(
json.dumps(vehicle_state),
account.config.log_response_path,
f"state_{vehicle_base['vin']}",
)
async def setup_mocked_integration(hass: HomeAssistant) -> MockConfigEntry:
"""Mock a fully setup config entry and all components based on fixtures."""
mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY)
mock_config_entry.add_to_hass(hass)
assert 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,12 @@
"""Fixtures for BMW tests."""
from bimmer_connected.account import MyBMWAccount
import pytest
from . import mock_vehicles_from_fixture
@pytest.fixture
async def bmw_fixture(monkeypatch):
"""Patch the vehicle fixtures into a MyBMWAccount."""
monkeypatch.setattr(MyBMWAccount, "get_vehicles", mock_vehicles_from_fixture)

View File

@ -0,0 +1,206 @@
{
"capabilities": {
"climateFunction": "AIR_CONDITIONING",
"climateNow": true,
"climateTimerTrigger": "DEPARTURE_TIMER",
"horn": true,
"isBmwChargingSupported": true,
"isCarSharingSupported": false,
"isChargeNowForBusinessSupported": false,
"isChargingHistorySupported": true,
"isChargingHospitalityEnabled": false,
"isChargingLoudnessEnabled": false,
"isChargingPlanSupported": true,
"isChargingPowerLimitEnabled": false,
"isChargingSettingsEnabled": false,
"isChargingTargetSocEnabled": false,
"isClimateTimerSupported": true,
"isCustomerEsimSupported": false,
"isDCSContractManagementSupported": true,
"isDataPrivacyEnabled": false,
"isEasyChargeEnabled": false,
"isEvGoChargingSupported": false,
"isMiniChargingSupported": false,
"isNonLscFeatureEnabled": false,
"isRemoteEngineStartSupported": false,
"isRemoteHistoryDeletionSupported": false,
"isRemoteHistorySupported": true,
"isRemoteParkingSupported": false,
"isRemoteServicesActivationRequired": false,
"isRemoteServicesBookingRequired": false,
"isScanAndChargeSupported": false,
"isSustainabilitySupported": false,
"isWifiHotspotServiceSupported": false,
"lastStateCallState": "ACTIVATED",
"lights": true,
"lock": true,
"remoteChargingCommands": {},
"sendPoi": true,
"specialThemeSupport": [],
"unlock": true,
"vehicleFinder": false,
"vehicleStateSource": "LAST_STATE_CALL"
},
"state": {
"chargingProfile": {
"chargingControlType": "WEEKLY_PLANNER",
"chargingMode": "DELAYED_CHARGING",
"chargingPreference": "CHARGING_WINDOW",
"chargingSettings": {
"hospitality": "NO_ACTION",
"idcc": "NO_ACTION",
"targetSoc": 100
},
"climatisationOn": false,
"departureTimes": [
{
"action": "DEACTIVATE",
"id": 1,
"timeStamp": {
"hour": 7,
"minute": 35
},
"timerWeekDays": [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY"
]
},
{
"action": "DEACTIVATE",
"id": 2,
"timeStamp": {
"hour": 18,
"minute": 0
},
"timerWeekDays": [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY",
"SATURDAY",
"SUNDAY"
]
},
{
"action": "DEACTIVATE",
"id": 3,
"timeStamp": {
"hour": 7,
"minute": 0
},
"timerWeekDays": []
},
{
"action": "DEACTIVATE",
"id": 4,
"timerWeekDays": []
}
],
"reductionOfChargeCurrent": {
"end": {
"hour": 1,
"minute": 30
},
"start": {
"hour": 18,
"minute": 1
}
}
},
"checkControlMessages": [],
"climateTimers": [
{
"departureTime": {
"hour": 6,
"minute": 40
},
"isWeeklyTimer": true,
"timerAction": "ACTIVATE",
"timerWeekDays": ["THURSDAY", "SUNDAY"]
},
{
"departureTime": {
"hour": 12,
"minute": 50
},
"isWeeklyTimer": false,
"timerAction": "ACTIVATE",
"timerWeekDays": ["MONDAY"]
},
{
"departureTime": {
"hour": 18,
"minute": 59
},
"isWeeklyTimer": true,
"timerAction": "DEACTIVATE",
"timerWeekDays": ["WEDNESDAY"]
}
],
"combustionFuelLevel": {
"range": 105,
"remainingFuelLiters": 6,
"remainingFuelPercent": 65
},
"currentMileage": 137009,
"doorsState": {
"combinedSecurityState": "UNLOCKED",
"combinedState": "CLOSED",
"hood": "CLOSED",
"leftFront": "CLOSED",
"leftRear": "CLOSED",
"rightFront": "CLOSED",
"rightRear": "CLOSED",
"trunk": "CLOSED"
},
"driverPreferences": {
"lscPrivacyMode": "OFF"
},
"electricChargingState": {
"chargingConnectionType": "CONDUCTIVE",
"chargingLevelPercent": 82,
"chargingStatus": "WAITING_FOR_CHARGING",
"chargingTarget": 100,
"isChargerConnected": true,
"range": 174
},
"isLeftSteering": true,
"isLscSupported": true,
"lastFetched": "2022-06-22T14:24:23.982Z",
"lastUpdatedAt": "2022-06-22T13:58:52Z",
"range": 174,
"requiredServices": [
{
"dateTime": "2022-10-01T00:00:00.000Z",
"description": "Next service due by the specified date.",
"status": "OK",
"type": "BRAKE_FLUID"
},
{
"dateTime": "2023-05-01T00:00:00.000Z",
"description": "Next vehicle check due after the specified distance or date.",
"status": "OK",
"type": "VEHICLE_CHECK"
},
{
"dateTime": "2023-05-01T00:00:00.000Z",
"description": "Next state inspection due by the specified date.",
"status": "OK",
"type": "VEHICLE_TUV"
}
],
"roofState": {
"roofState": "CLOSED",
"roofStateType": "SUN_ROOF"
},
"windowsState": {
"combinedState": "CLOSED",
"leftFront": "CLOSED",
"rightFront": "CLOSED"
}
}
}

View File

@ -0,0 +1,47 @@
[
{
"appVehicleType": "CONNECTED",
"attributes": {
"a4aType": "USB_ONLY",
"bodyType": "I01",
"brand": "BMW_I",
"color": 4284110934,
"countryOfOrigin": "CZ",
"driveTrain": "ELECTRIC_WITH_RANGE_EXTENDER",
"driverGuideInfo": {
"androidAppScheme": "com.bmwgroup.driversguide.row",
"androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row",
"iosAppScheme": "bmwdriversguide:///open",
"iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8"
},
"headUnitType": "NBT",
"hmiVersion": "ID4",
"lastFetched": "2022-07-10T09:25:53.104Z",
"model": "i3 (+ REX)",
"softwareVersionCurrent": {
"iStep": 510,
"puStep": {
"month": 11,
"year": 21
},
"seriesCluster": "I001"
},
"softwareVersionExFactory": {
"iStep": 502,
"puStep": {
"month": 3,
"year": 15
},
"seriesCluster": "I001"
},
"year": 2015
},
"mappingInfo": {
"isAssociated": false,
"isLmmEnabled": false,
"isPrimaryUser": true,
"mappingStatus": "CONFIRMED"
},
"vin": "WBY00000000REXI01"
}
]

View File

@ -12,15 +12,11 @@ from homeassistant.components.bmw_connected_drive.const import (
) )
from homeassistant.const import CONF_USERNAME from homeassistant.const import CONF_USERNAME
from . import FIXTURE_CONFIG_ENTRY, FIXTURE_USER_INPUT from . import FIXTURE_CONFIG_ENTRY, FIXTURE_REFRESH_TOKEN, FIXTURE_USER_INPUT
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
FIXTURE_REFRESH_TOKEN = "SOME_REFRESH_TOKEN" FIXTURE_COMPLETE_ENTRY = FIXTURE_CONFIG_ENTRY["data"]
FIXTURE_COMPLETE_ENTRY = {
**FIXTURE_USER_INPUT,
CONF_REFRESH_TOKEN: FIXTURE_REFRESH_TOKEN,
}
FIXTURE_IMPORT_ENTRY = {**FIXTURE_USER_INPUT, CONF_REFRESH_TOKEN: None} FIXTURE_IMPORT_ENTRY = {**FIXTURE_USER_INPUT, CONF_REFRESH_TOKEN: None}

View File

@ -0,0 +1,52 @@
"""Test BMW sensors."""
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.util.unit_system import (
IMPERIAL_SYSTEM as IMPERIAL,
METRIC_SYSTEM as METRIC,
UnitSystem,
)
from . import setup_mocked_integration
@pytest.mark.parametrize(
"entity_id,unit_system,value,unit_of_measurement",
[
("sensor.i3_rex_remaining_range_total", METRIC, "279", "km"),
("sensor.i3_rex_remaining_range_total", IMPERIAL, "173.36", "mi"),
("sensor.i3_rex_mileage", METRIC, "137009", "km"),
("sensor.i3_rex_mileage", IMPERIAL, "85133.42", "mi"),
("sensor.i3_rex_remaining_battery_percent", METRIC, "82", "%"),
("sensor.i3_rex_remaining_battery_percent", IMPERIAL, "82", "%"),
("sensor.i3_rex_remaining_range_electric", METRIC, "174", "km"),
("sensor.i3_rex_remaining_range_electric", IMPERIAL, "108.12", "mi"),
("sensor.i3_rex_remaining_fuel", METRIC, "6", "L"),
("sensor.i3_rex_remaining_fuel", IMPERIAL, "1.59", "gal"),
("sensor.i3_rex_remaining_range_fuel", METRIC, "105", "km"),
("sensor.i3_rex_remaining_range_fuel", IMPERIAL, "65.24", "mi"),
("sensor.i3_rex_remaining_fuel_percent", METRIC, "65", "%"),
("sensor.i3_rex_remaining_fuel_percent", IMPERIAL, "65", "%"),
],
)
async def test_unit_conversion(
hass: HomeAssistant,
entity_id: str,
unit_system: UnitSystem,
value: str,
unit_of_measurement: str,
bmw_fixture,
) -> None:
"""Test conversion between metric and imperial units for sensors."""
# Set unit system
hass.config.units = unit_system
# Setup component
assert await setup_mocked_integration(hass)
# Test
entity = hass.states.get(entity_id)
assert entity.state == value
assert entity.attributes.get("unit_of_measurement") == unit_of_measurement

View File

@ -1,15 +1,34 @@
"""Entity tests for mobile_app.""" """Entity tests for mobile_app."""
from http import HTTPStatus from http import HTTPStatus
from unittest.mock import patch
import pytest import pytest
from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import PERCENTAGE, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.const import (
PERCENTAGE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
async def test_sensor(hass, create_registrations, webhook_client): @pytest.mark.parametrize(
"unit_system, state_unit, state1, state2",
(
(METRIC_SYSTEM, TEMP_CELSIUS, "100", "123"),
(IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "253"),
),
)
async def test_sensor(
hass, create_registrations, webhook_client, unit_system, state_unit, state1, state2
):
"""Test that sensors can be registered and updated.""" """Test that sensors can be registered and updated."""
hass.config.units = unit_system
webhook_id = create_registrations[1]["webhook_id"] webhook_id = create_registrations[1]["webhook_id"]
webhook_url = f"/api/webhook/{webhook_id}" webhook_url = f"/api/webhook/{webhook_id}"
@ -19,15 +38,15 @@ async def test_sensor(hass, create_registrations, webhook_client):
"type": "register_sensor", "type": "register_sensor",
"data": { "data": {
"attributes": {"foo": "bar"}, "attributes": {"foo": "bar"},
"device_class": "battery", "device_class": "temperature",
"icon": "mdi:battery", "icon": "mdi:battery",
"name": "Battery State", "name": "Battery Temperature",
"state": 100, "state": 100,
"type": "sensor", "type": "sensor",
"entity_category": "diagnostic", "entity_category": "diagnostic",
"unique_id": "battery_state", "unique_id": "battery_temp",
"state_class": "total", "state_class": "total",
"unit_of_measurement": PERCENTAGE, "unit_of_measurement": TEMP_CELSIUS,
}, },
}, },
) )
@ -38,20 +57,23 @@ async def test_sensor(hass, create_registrations, webhook_client):
assert json == {"success": True} assert json == {"success": True}
await hass.async_block_till_done() await hass.async_block_till_done()
entity = hass.states.get("sensor.test_1_battery_state") entity = hass.states.get("sensor.test_1_battery_temperature")
assert entity is not None assert entity is not None
assert entity.attributes["device_class"] == "battery" assert entity.attributes["device_class"] == "temperature"
assert entity.attributes["icon"] == "mdi:battery" assert entity.attributes["icon"] == "mdi:battery"
assert entity.attributes["unit_of_measurement"] == PERCENTAGE # unit of temperature sensor is automatically converted to the system UoM
assert entity.attributes["unit_of_measurement"] == state_unit
assert entity.attributes["foo"] == "bar" assert entity.attributes["foo"] == "bar"
assert entity.attributes["state_class"] == "total" assert entity.attributes["state_class"] == "total"
assert entity.domain == "sensor" assert entity.domain == "sensor"
assert entity.name == "Test 1 Battery State" assert entity.name == "Test 1 Battery Temperature"
assert entity.state == "100" assert entity.state == state1
assert ( assert (
er.async_get(hass).async_get("sensor.test_1_battery_state").entity_category er.async_get(hass)
.async_get("sensor.test_1_battery_temperature")
.entity_category
== "diagnostic" == "diagnostic"
) )
@ -64,7 +86,7 @@ async def test_sensor(hass, create_registrations, webhook_client):
"icon": "mdi:battery-unknown", "icon": "mdi:battery-unknown",
"state": 123, "state": 123,
"type": "sensor", "type": "sensor",
"unique_id": "battery_state", "unique_id": "battery_temp",
}, },
# This invalid data should not invalidate whole request # This invalid data should not invalidate whole request
{"type": "sensor", "unique_id": "invalid_state", "invalid": "data"}, {"type": "sensor", "unique_id": "invalid_state", "invalid": "data"},
@ -77,8 +99,8 @@ async def test_sensor(hass, create_registrations, webhook_client):
json = await update_resp.json() json = await update_resp.json()
assert json["invalid_state"]["success"] is False assert json["invalid_state"]["success"] is False
updated_entity = hass.states.get("sensor.test_1_battery_state") updated_entity = hass.states.get("sensor.test_1_battery_temperature")
assert updated_entity.state == "123" assert updated_entity.state == state2
assert "foo" not in updated_entity.attributes assert "foo" not in updated_entity.attributes
dev_reg = dr.async_get(hass) dev_reg = dr.async_get(hass)
@ -88,16 +110,120 @@ async def test_sensor(hass, create_registrations, webhook_client):
config_entry = hass.config_entries.async_entries("mobile_app")[1] config_entry = hass.config_entries.async_entries("mobile_app")[1]
await hass.config_entries.async_unload(config_entry.entry_id) await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
unloaded_entity = hass.states.get("sensor.test_1_battery_state") unloaded_entity = hass.states.get("sensor.test_1_battery_temperature")
assert unloaded_entity.state == STATE_UNAVAILABLE assert unloaded_entity.state == STATE_UNAVAILABLE
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
restored_entity = hass.states.get("sensor.test_1_battery_state") restored_entity = hass.states.get("sensor.test_1_battery_temperature")
assert restored_entity.state == updated_entity.state assert restored_entity.state == updated_entity.state
assert restored_entity.attributes == updated_entity.attributes assert restored_entity.attributes == updated_entity.attributes
@pytest.mark.parametrize(
"unique_id, unit_system, state_unit, state1, state2",
(
("battery_temperature", METRIC_SYSTEM, TEMP_CELSIUS, "100", "123"),
("battery_temperature", IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "253"),
# The unique_id doesn't match that of the mobile app's battery temperature sensor
("battery_temp", IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "123"),
),
)
async def test_sensor_migration(
hass,
create_registrations,
webhook_client,
unique_id,
unit_system,
state_unit,
state1,
state2,
):
"""Test migration to RestoreSensor."""
hass.config.units = unit_system
webhook_id = create_registrations[1]["webhook_id"]
webhook_url = f"/api/webhook/{webhook_id}"
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"attributes": {"foo": "bar"},
"device_class": "temperature",
"icon": "mdi:battery",
"name": "Battery Temperature",
"state": 100,
"type": "sensor",
"entity_category": "diagnostic",
"unique_id": unique_id,
"state_class": "total",
"unit_of_measurement": TEMP_CELSIUS,
},
},
)
assert reg_resp.status == HTTPStatus.CREATED
json = await reg_resp.json()
assert json == {"success": True}
await hass.async_block_till_done()
entity = hass.states.get("sensor.test_1_battery_temperature")
assert entity is not None
assert entity.attributes["device_class"] == "temperature"
assert entity.attributes["icon"] == "mdi:battery"
# unit of temperature sensor is automatically converted to the system UoM
assert entity.attributes["unit_of_measurement"] == state_unit
assert entity.attributes["foo"] == "bar"
assert entity.attributes["state_class"] == "total"
assert entity.domain == "sensor"
assert entity.name == "Test 1 Battery Temperature"
assert entity.state == state1
# Reload to verify state is restored
config_entry = hass.config_entries.async_entries("mobile_app")[1]
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
unloaded_entity = hass.states.get("sensor.test_1_battery_temperature")
assert unloaded_entity.state == STATE_UNAVAILABLE
# Simulate migration to RestoreSensor
with patch(
"homeassistant.helpers.restore_state.RestoreEntity.async_get_last_extra_data",
return_value=None,
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
restored_entity = hass.states.get("sensor.test_1_battery_temperature")
assert restored_entity.state == "unknown"
assert restored_entity.attributes == entity.attributes
# Test unit conversion is working
update_resp = await webhook_client.post(
webhook_url,
json={
"type": "update_sensor_states",
"data": [
{
"icon": "mdi:battery-unknown",
"state": 123,
"type": "sensor",
"unique_id": unique_id,
},
],
},
)
assert update_resp.status == HTTPStatus.OK
updated_entity = hass.states.get("sensor.test_1_battery_temperature")
assert updated_entity.state == state2
assert "foo" not in updated_entity.attributes
async def test_sensor_must_register(hass, create_registrations, webhook_client): async def test_sensor_must_register(hass, create_registrations, webhook_client):
"""Test that sensors must be registered before updating.""" """Test that sensors must be registered before updating."""
webhook_id = create_registrations[1]["webhook_id"] webhook_id = create_registrations[1]["webhook_id"]

View File

@ -1,12 +1,13 @@
"""The tests for notify services that change targets.""" """The tests for notify services that change targets."""
import asyncio
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import yaml import yaml
from homeassistant import config as hass_config from homeassistant import config as hass_config
from homeassistant.components import notify from homeassistant.components import notify
from homeassistant.const import SERVICE_RELOAD from homeassistant.const import SERVICE_RELOAD, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.reload import async_setup_reload_service
@ -330,3 +331,99 @@ async def test_setup_platform_and_reload(hass, caplog, tmp_path):
# Check if the dynamically notify services from setup were removed # Check if the dynamically notify services from setup were removed
assert not hass.services.has_service(notify.DOMAIN, "testnotify2_c") assert not hass.services.has_service(notify.DOMAIN, "testnotify2_c")
assert not hass.services.has_service(notify.DOMAIN, "testnotify2_d") assert not hass.services.has_service(notify.DOMAIN, "testnotify2_d")
async def test_setup_platform_before_notify_setup(hass, caplog, tmp_path):
"""Test trying to setup a platform before notify is setup."""
get_service_called = Mock()
async def async_get_service(hass, config, discovery_info=None):
"""Get notify service for mocked platform."""
get_service_called(config, discovery_info)
targetlist = {"a": 1, "b": 2}
return NotificationService(hass, targetlist, "testnotify")
async def async_get_service2(hass, config, discovery_info=None):
"""Get notify service for mocked platform."""
get_service_called(config, discovery_info)
targetlist = {"c": 3, "d": 4}
return NotificationService(hass, targetlist, "testnotify2")
# Mock first platform
mock_notify_platform(
hass, tmp_path, "testnotify", async_get_service=async_get_service
)
# Initialize a second platform testnotify2
mock_notify_platform(
hass, tmp_path, "testnotify2", async_get_service=async_get_service2
)
hass_config = {"notify": [{"platform": "testnotify"}]}
# Setup the second testnotify2 platform from discovery
load_coro = async_load_platform(
hass, Platform.NOTIFY, "testnotify2", {}, hass_config=hass_config
)
# Setup the testnotify platform
setup_coro = async_setup_component(hass, "notify", hass_config)
load_task = asyncio.create_task(load_coro)
setup_task = asyncio.create_task(setup_coro)
await asyncio.gather(load_task, setup_task)
await hass.async_block_till_done()
assert hass.services.has_service(notify.DOMAIN, "testnotify_a")
assert hass.services.has_service(notify.DOMAIN, "testnotify_b")
assert hass.services.has_service(notify.DOMAIN, "testnotify2_c")
assert hass.services.has_service(notify.DOMAIN, "testnotify2_d")
async def test_setup_platform_after_notify_setup(hass, caplog, tmp_path):
"""Test trying to setup a platform after notify is setup."""
get_service_called = Mock()
async def async_get_service(hass, config, discovery_info=None):
"""Get notify service for mocked platform."""
get_service_called(config, discovery_info)
targetlist = {"a": 1, "b": 2}
return NotificationService(hass, targetlist, "testnotify")
async def async_get_service2(hass, config, discovery_info=None):
"""Get notify service for mocked platform."""
get_service_called(config, discovery_info)
targetlist = {"c": 3, "d": 4}
return NotificationService(hass, targetlist, "testnotify2")
# Mock first platform
mock_notify_platform(
hass, tmp_path, "testnotify", async_get_service=async_get_service
)
# Initialize a second platform testnotify2
mock_notify_platform(
hass, tmp_path, "testnotify2", async_get_service=async_get_service2
)
hass_config = {"notify": [{"platform": "testnotify"}]}
# Setup the second testnotify2 platform from discovery
load_coro = async_load_platform(
hass, Platform.NOTIFY, "testnotify2", {}, hass_config=hass_config
)
# Setup the testnotify platform
setup_coro = async_setup_component(hass, "notify", hass_config)
setup_task = asyncio.create_task(setup_coro)
load_task = asyncio.create_task(load_coro)
await asyncio.gather(load_task, setup_task)
await hass.async_block_till_done()
assert hass.services.has_service(notify.DOMAIN, "testnotify_a")
assert hass.services.has_service(notify.DOMAIN, "testnotify_b")
assert hass.services.has_service(notify.DOMAIN, "testnotify2_c")
assert hass.services.has_service(notify.DOMAIN, "testnotify2_d")