Merge pull request #76964 from home-assistant/rc

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

View File

@ -16,6 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .base import AcmedaBase
from .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])

View File

@ -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:

View File

@ -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]
)
)

View File

@ -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"

View File

@ -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

View File

@ -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] = {

View File

@ -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."""

View File

@ -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,

View File

@ -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."""

View File

@ -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:

View File

@ -111,8 +111,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) or OVERKIZ_DEVICE_TO_PLATFORM.get(device.ui_class):
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

View File

@ -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],
)
)

View File

@ -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"

View File

@ -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)

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,12 @@
"""Fixtures for BMW tests."""
from bimmer_connected.account import MyBMWAccount
import pytest
from . import mock_vehicles_from_fixture
@pytest.fixture
async def bmw_fixture(monkeypatch):
"""Patch the vehicle fixtures into a MyBMWAccount."""
monkeypatch.setattr(MyBMWAccount, "get_vehicles", mock_vehicles_from_fixture)

View File

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

View File

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

View File

@ -12,15 +12,11 @@ from homeassistant.components.bmw_connected_drive.const import (
)
from homeassistant.const import CONF_USERNAME
from . 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}

View File

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

View File

@ -1,15 +1,34 @@
"""Entity tests for mobile_app."""
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"]

View File

@ -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")