mirror of
https://github.com/home-assistant/core.git
synced 2025-07-26 06:37:52 +00:00
Merge pull request #76964 from home-assistant/rc
This commit is contained in:
commit
c894ddeb95
@ -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])
|
||||||
|
@ -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:
|
||||||
|
@ -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],
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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] = {
|
||||||
|
@ -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."""
|
||||||
|
@ -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,
|
||||||
|
@ -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."""
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
12
tests/components/bmw_connected_drive/conftest.py
Normal file
12
tests/components/bmw_connected_drive/conftest.py
Normal 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)
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
@ -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}
|
||||||
|
|
||||||
|
|
||||||
|
52
tests/components/bmw_connected_drive/test_sensor.py
Normal file
52
tests/components/bmw_connected_drive/test_sensor.py
Normal 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
|
@ -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"]
|
||||||
|
@ -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")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user