commit
c894ddeb95
|
@ -16,6 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||
from .base import AcmedaBase
|
||||
from .const import ACMEDA_HUB_UPDATE, DOMAIN
|
||||
from .helpers import async_add_acmeda_entities
|
||||
from .hub import PulseHub
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
@ -24,7 +25,7 @@ async def async_setup_entry(
|
|||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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()
|
||||
|
||||
|
@ -122,6 +123,6 @@ class AcmedaCover(AcmedaBase, CoverEntity):
|
|||
"""Stop the roller."""
|
||||
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."""
|
||||
await self.roller.move_to(100 - kwargs[ATTR_POSITION])
|
||||
|
|
|
@ -15,13 +15,7 @@ from homeassistant.components.sensor import (
|
|||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
LENGTH_KILOMETERS,
|
||||
LENGTH_MILES,
|
||||
PERCENTAGE,
|
||||
VOLUME_GALLONS,
|
||||
VOLUME_LITERS,
|
||||
)
|
||||
from homeassistant.const import LENGTH, PERCENTAGE, VOLUME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
@ -39,8 +33,7 @@ class BMWSensorEntityDescription(SensorEntityDescription):
|
|||
"""Describes BMW sensor entity."""
|
||||
|
||||
key_class: str | None = None
|
||||
unit_metric: str | None = None
|
||||
unit_imperial: str | None = None
|
||||
unit_type: str | None = None
|
||||
value: Callable = lambda x, y: x
|
||||
|
||||
|
||||
|
@ -86,56 +79,49 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = {
|
|||
"remaining_battery_percent": BMWSensorEntityDescription(
|
||||
key="remaining_battery_percent",
|
||||
key_class="fuel_and_battery",
|
||||
unit_metric=PERCENTAGE,
|
||||
unit_imperial=PERCENTAGE,
|
||||
unit_type=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
),
|
||||
# --- Specific ---
|
||||
"mileage": BMWSensorEntityDescription(
|
||||
key="mileage",
|
||||
icon="mdi:speedometer",
|
||||
unit_metric=LENGTH_KILOMETERS,
|
||||
unit_imperial=LENGTH_MILES,
|
||||
unit_type=LENGTH,
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
|
||||
),
|
||||
"remaining_range_total": BMWSensorEntityDescription(
|
||||
key="remaining_range_total",
|
||||
key_class="fuel_and_battery",
|
||||
icon="mdi:map-marker-distance",
|
||||
unit_metric=LENGTH_KILOMETERS,
|
||||
unit_imperial=LENGTH_MILES,
|
||||
unit_type=LENGTH,
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
|
||||
),
|
||||
"remaining_range_electric": BMWSensorEntityDescription(
|
||||
key="remaining_range_electric",
|
||||
key_class="fuel_and_battery",
|
||||
icon="mdi:map-marker-distance",
|
||||
unit_metric=LENGTH_KILOMETERS,
|
||||
unit_imperial=LENGTH_MILES,
|
||||
unit_type=LENGTH,
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
|
||||
),
|
||||
"remaining_range_fuel": BMWSensorEntityDescription(
|
||||
key="remaining_range_fuel",
|
||||
key_class="fuel_and_battery",
|
||||
icon="mdi:map-marker-distance",
|
||||
unit_metric=LENGTH_KILOMETERS,
|
||||
unit_imperial=LENGTH_MILES,
|
||||
unit_type=LENGTH,
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
|
||||
),
|
||||
"remaining_fuel": BMWSensorEntityDescription(
|
||||
key="remaining_fuel",
|
||||
key_class="fuel_and_battery",
|
||||
icon="mdi:gas-station",
|
||||
unit_metric=VOLUME_LITERS,
|
||||
unit_imperial=VOLUME_GALLONS,
|
||||
unit_type=VOLUME,
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2),
|
||||
),
|
||||
"remaining_fuel_percent": BMWSensorEntityDescription(
|
||||
key="remaining_fuel_percent",
|
||||
key_class="fuel_and_battery",
|
||||
icon="mdi:gas-station",
|
||||
unit_metric=PERCENTAGE,
|
||||
unit_imperial=PERCENTAGE,
|
||||
unit_type=PERCENTAGE,
|
||||
),
|
||||
}
|
||||
|
||||
|
@ -182,8 +168,12 @@ class BMWSensor(BMWBaseEntity, SensorEntity):
|
|||
self._attr_name = f"{vehicle.name} {description.key}"
|
||||
self._attr_unique_id = f"{vehicle.vin}-{description.key}"
|
||||
|
||||
# Force metric system as BMW API apparently only returns metric values now
|
||||
self._attr_native_unit_of_measurement = description.unit_metric
|
||||
# Set the correct unit of measurement based on the unit_type
|
||||
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
|
||||
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.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
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]
|
||||
|
||||
|
||||
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:
|
||||
"""Set up Discord from a config entry."""
|
||||
nextcord.VoiceClient.warn_nacl = False
|
||||
|
@ -30,11 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
hass,
|
||||
Platform.NOTIFY,
|
||||
DOMAIN,
|
||||
hass.data[DOMAIN][entry.entry_id],
|
||||
hass.data[DOMAIN],
|
||||
hass, Platform.NOTIFY, DOMAIN, dict(entry.data), hass.data[DATA_HASS_CONFIG]
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -8,3 +8,5 @@ DEFAULT_NAME = "Discord"
|
|||
DOMAIN: Final = "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 self._config[ATTR_SENSOR_STATE]
|
||||
|
||||
@callback
|
||||
def async_restore_last_state(self, last_state):
|
||||
async def async_restore_last_state(self, last_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
|
||||
|
|
|
@ -43,10 +43,9 @@ class MobileAppEntity(RestoreEntity):
|
|||
if (state := await self.async_get_last_state()) is None:
|
||||
return
|
||||
|
||||
self.async_restore_last_state(state)
|
||||
await self.async_restore_last_state(state)
|
||||
|
||||
@callback
|
||||
def async_restore_last_state(self, last_state):
|
||||
async def async_restore_last_state(self, last_state):
|
||||
"""Restore previous state."""
|
||||
self._config[ATTR_SENSOR_STATE] = last_state.state
|
||||
self._config[ATTR_SENSOR_ATTRIBUTES] = {
|
||||
|
|
|
@ -3,9 +3,9 @@ from __future__ import annotations
|
|||
|
||||
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.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.helpers import entity_registry as er
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
@ -27,6 +27,7 @@ from .const import (
|
|||
DOMAIN,
|
||||
)
|
||||
from .entity import MobileAppEntity
|
||||
from .webhook import _extract_sensor_unique_id
|
||||
|
||||
|
||||
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."""
|
||||
|
||||
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
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"domain": "netgear",
|
||||
"name": "NETGEAR",
|
||||
"documentation": "https://www.home-assistant.io/integrations/netgear",
|
||||
"requirements": ["pynetgear==0.10.6"],
|
||||
"requirements": ["pynetgear==0.10.7"],
|
||||
"codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"],
|
||||
"iot_class": "local_polling",
|
||||
"config_flow": true,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""Provides functionality to notify people."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.persistent_notification as pn
|
||||
|
@ -40,13 +42,19 @@ PLATFORM_SCHEMA = vol.Schema(
|
|||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the notify services."""
|
||||
|
||||
platform_setups = async_setup_legacy(hass, config)
|
||||
|
||||
# We need to add the component here break the deadlock
|
||||
# when setting up integrations from config entries as
|
||||
# they would otherwise wait for notify to be
|
||||
# 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)
|
||||
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:
|
||||
"""Send notification via the built-in persistsent_notify integration."""
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Coroutine
|
||||
from functools import partial
|
||||
from typing import Any, cast
|
||||
|
||||
|
@ -32,7 +33,10 @@ NOTIFY_SERVICES = "notify_services"
|
|||
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."""
|
||||
hass.data.setdefault(NOTIFY_SERVICES, {})
|
||||
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}")
|
||||
|
||||
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(
|
||||
platform: str, info: DiscoveryInfoType | None
|
||||
) -> None:
|
||||
|
@ -120,6 +115,12 @@ async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None:
|
|||
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
|
||||
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):
|
||||
platforms[platform].append(device)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
for gateway in setup.gateways:
|
||||
|
@ -128,6 +126,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
configuration_url=server.configuration_url,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
|||
from homeassistant.helpers import aiohttp_client, discovery
|
||||
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__)
|
||||
|
||||
|
@ -21,6 +21,8 @@ PLATFORMS = [Platform.NOTIFY]
|
|||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Slack component."""
|
||||
hass.data[DATA_HASS_CONFIG] = config
|
||||
|
||||
# Iterate all entries for notify to only get Slack
|
||||
if Platform.NOTIFY in config:
|
||||
for entry in config[Platform.NOTIFY]:
|
||||
|
@ -55,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
Platform.NOTIFY,
|
||||
DOMAIN,
|
||||
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"
|
||||
DEFAULT_TIMEOUT_SECONDS = 15
|
||||
DOMAIN: Final = "slack"
|
||||
|
||||
DATA_HASS_CONFIG = "slack_hass_config"
|
||||
|
|
|
@ -7,7 +7,7 @@ from .backports.enum import StrEnum
|
|||
|
||||
MAJOR_VERSION: Final = 2022
|
||||
MINOR_VERSION: Final = 8
|
||||
PATCH_VERSION: Final = "5"
|
||||
PATCH_VERSION: Final = "6"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
|
||||
|
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2022.8.5"
|
||||
version = "2022.8.6"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
|
|
@ -1683,7 +1683,7 @@ pymyq==3.1.4
|
|||
pymysensors==0.22.1
|
||||
|
||||
# homeassistant.components.netgear
|
||||
pynetgear==0.10.6
|
||||
pynetgear==0.10.7
|
||||
|
||||
# homeassistant.components.netio
|
||||
pynetio==0.1.9.1
|
||||
|
|
|
@ -1160,7 +1160,7 @@ pymyq==3.1.4
|
|||
pymysensors==0.22.1
|
||||
|
||||
# homeassistant.components.netgear
|
||||
pynetgear==0.10.6
|
||||
pynetgear==0.10.7
|
||||
|
||||
# homeassistant.components.nina
|
||||
pynina==0.1.8
|
||||
|
|
|
@ -1,17 +1,29 @@
|
|||
"""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.components.bmw_connected_drive.const import (
|
||||
CONF_READ_ONLY,
|
||||
CONF_REFRESH_TOKEN,
|
||||
DOMAIN as BMW_DOMAIN,
|
||||
)
|
||||
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 = {
|
||||
CONF_USERNAME: "user@domain.com",
|
||||
CONF_PASSWORD: "p4ssw0rd",
|
||||
CONF_REGION: "rest_of_world",
|
||||
}
|
||||
FIXTURE_REFRESH_TOKEN = "SOME_REFRESH_TOKEN"
|
||||
|
||||
FIXTURE_CONFIG_ENTRY = {
|
||||
"entry_id": "1",
|
||||
|
@ -21,8 +33,82 @@ FIXTURE_CONFIG_ENTRY = {
|
|||
CONF_USERNAME: FIXTURE_USER_INPUT[CONF_USERNAME],
|
||||
CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD],
|
||||
CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION],
|
||||
CONF_REFRESH_TOKEN: FIXTURE_REFRESH_TOKEN,
|
||||
},
|
||||
"options": {CONF_READ_ONLY: False},
|
||||
"source": config_entries.SOURCE_USER,
|
||||
"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
|
||||
|
|
|
@ -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 . import FIXTURE_CONFIG_ENTRY, FIXTURE_USER_INPUT
|
||||
from . import FIXTURE_CONFIG_ENTRY, FIXTURE_REFRESH_TOKEN, FIXTURE_USER_INPUT
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
FIXTURE_REFRESH_TOKEN = "SOME_REFRESH_TOKEN"
|
||||
FIXTURE_COMPLETE_ENTRY = {
|
||||
**FIXTURE_USER_INPUT,
|
||||
CONF_REFRESH_TOKEN: FIXTURE_REFRESH_TOKEN,
|
||||
}
|
||||
FIXTURE_COMPLETE_ENTRY = FIXTURE_CONFIG_ENTRY["data"]
|
||||
FIXTURE_IMPORT_ENTRY = {**FIXTURE_USER_INPUT, CONF_REFRESH_TOKEN: None}
|
||||
|
||||
|
||||
|
|
|
@ -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."""
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
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.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."""
|
||||
hass.config.units = unit_system
|
||||
|
||||
webhook_id = create_registrations[1]["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",
|
||||
"data": {
|
||||
"attributes": {"foo": "bar"},
|
||||
"device_class": "battery",
|
||||
"device_class": "temperature",
|
||||
"icon": "mdi:battery",
|
||||
"name": "Battery State",
|
||||
"name": "Battery Temperature",
|
||||
"state": 100,
|
||||
"type": "sensor",
|
||||
"entity_category": "diagnostic",
|
||||
"unique_id": "battery_state",
|
||||
"unique_id": "battery_temp",
|
||||
"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}
|
||||
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.attributes["device_class"] == "battery"
|
||||
assert entity.attributes["device_class"] == "temperature"
|
||||
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["state_class"] == "total"
|
||||
assert entity.domain == "sensor"
|
||||
assert entity.name == "Test 1 Battery State"
|
||||
assert entity.state == "100"
|
||||
assert entity.name == "Test 1 Battery Temperature"
|
||||
assert entity.state == state1
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
|
@ -64,7 +86,7 @@ async def test_sensor(hass, create_registrations, webhook_client):
|
|||
"icon": "mdi:battery-unknown",
|
||||
"state": 123,
|
||||
"type": "sensor",
|
||||
"unique_id": "battery_state",
|
||||
"unique_id": "battery_temp",
|
||||
},
|
||||
# This invalid data should not invalidate whole request
|
||||
{"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()
|
||||
assert json["invalid_state"]["success"] is False
|
||||
|
||||
updated_entity = hass.states.get("sensor.test_1_battery_state")
|
||||
assert updated_entity.state == "123"
|
||||
updated_entity = hass.states.get("sensor.test_1_battery_temperature")
|
||||
assert updated_entity.state == state2
|
||||
assert "foo" not in updated_entity.attributes
|
||||
|
||||
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]
|
||||
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_state")
|
||||
unloaded_entity = hass.states.get("sensor.test_1_battery_temperature")
|
||||
assert unloaded_entity.state == STATE_UNAVAILABLE
|
||||
|
||||
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_state")
|
||||
restored_entity = hass.states.get("sensor.test_1_battery_temperature")
|
||||
assert restored_entity.state == updated_entity.state
|
||||
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):
|
||||
"""Test that sensors must be registered before updating."""
|
||||
webhook_id = create_registrations[1]["webhook_id"]
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
"""The tests for notify services that change targets."""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import yaml
|
||||
|
||||
from homeassistant import config as hass_config
|
||||
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.helpers.discovery import async_load_platform
|
||||
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
|
||||
assert not hass.services.has_service(notify.DOMAIN, "testnotify2_c")
|
||||
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…
Reference in New Issue