Merge pull request #70054 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2022-04-14 13:36:07 -07:00 committed by GitHub
commit 30db51a49c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 399 additions and 105 deletions

View File

@ -28,6 +28,7 @@ TYPE_BATT6 = "batt6"
TYPE_BATT7 = "batt7" TYPE_BATT7 = "batt7"
TYPE_BATT8 = "batt8" TYPE_BATT8 = "batt8"
TYPE_BATT9 = "batt9" TYPE_BATT9 = "batt9"
TYPE_BATTIN = "battin"
TYPE_BATTOUT = "battout" TYPE_BATTOUT = "battout"
TYPE_BATT_CO2 = "batt_co2" TYPE_BATT_CO2 = "batt_co2"
TYPE_BATT_LIGHTNING = "batt_lightning" TYPE_BATT_LIGHTNING = "batt_lightning"
@ -140,6 +141,13 @@ BINARY_SENSOR_DESCRIPTIONS = (
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
on_state=0, on_state=0,
), ),
AmbientBinarySensorDescription(
key=TYPE_BATTIN,
name="Interior Battery",
device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
on_state=0,
),
AmbientBinarySensorDescription( AmbientBinarySensorDescription(
key=TYPE_BATT10, key=TYPE_BATT10,
name="Soil Monitor Battery 10", name="Soil Monitor Battery 10",

View File

@ -108,7 +108,7 @@ class BackupManager:
size=round(backup_path.stat().st_size / 1_048_576, 2), size=round(backup_path.stat().st_size / 1_048_576, 2),
) )
backups[backup.slug] = backup backups[backup.slug] = backup
except (OSError, TarError, json.JSONDecodeError) as err: except (OSError, TarError, json.JSONDecodeError, KeyError) as err:
LOGGER.warning("Unable to read backup %s: %s", backup_path, err) LOGGER.warning("Unable to read backup %s: %s", backup_path, err)
return backups return backups

View File

@ -134,10 +134,16 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
discovery_service_list = discovery_info.upnp.get(ssdp.ATTR_UPNP_SERVICE_LIST) discovery_service_list = discovery_info.upnp.get(ssdp.ATTR_UPNP_SERVICE_LIST)
if not discovery_service_list: if not discovery_service_list:
return self.async_abort(reason="not_dmr") return self.async_abort(reason="not_dmr")
discovery_service_ids = {
service.get("serviceId") services = discovery_service_list.get("service")
for service in discovery_service_list.get("service") or [] if not services:
} discovery_service_ids: set[str] = set()
elif isinstance(services, list):
discovery_service_ids = {service.get("serviceId") for service in services}
else:
# Only one service defined (etree_to_dict failed to make a list)
discovery_service_ids = {services.get("serviceId")}
if not DmrDevice.SERVICE_IDS.issubset(discovery_service_ids): if not DmrDevice.SERVICE_IDS.issubset(discovery_service_ids):
return self.async_abort(reason="not_dmr") return self.async_abort(reason="not_dmr")

View File

@ -77,10 +77,16 @@ class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
discovery_service_list = discovery_info.upnp.get(ssdp.ATTR_UPNP_SERVICE_LIST) discovery_service_list = discovery_info.upnp.get(ssdp.ATTR_UPNP_SERVICE_LIST)
if not discovery_service_list: if not discovery_service_list:
return self.async_abort(reason="not_dms") return self.async_abort(reason="not_dms")
discovery_service_ids = {
service.get("serviceId") services = discovery_service_list.get("service")
for service in discovery_service_list.get("service") or [] if not services:
} discovery_service_ids: set[str] = set()
elif isinstance(services, list):
discovery_service_ids = {service.get("serviceId") for service in services}
else:
# Only one service defined (etree_to_dict failed to make a list)
discovery_service_ids = {services.get("serviceId")}
if not DmsDevice.SERVICE_IDS.issubset(discovery_service_ids): if not DmsDevice.SERVICE_IDS.issubset(discovery_service_ids):
return self.async_abort(reason="not_dms") return self.async_abort(reason="not_dms")

View File

@ -2,7 +2,7 @@
"domain": "generic", "domain": "generic",
"name": "Generic Camera", "name": "Generic Camera",
"config_flow": true, "config_flow": true,
"requirements": ["av==8.1.0", "pillow==9.0.1"], "requirements": ["ha-av==9.1.1-3", "pillow==9.0.1"],
"documentation": "https://www.home-assistant.io/integrations/generic", "documentation": "https://www.home-assistant.io/integrations/generic",
"codeowners": ["@davet2001"], "codeowners": ["@davet2001"],
"iot_class": "local_push" "iot_class": "local_push"

View File

@ -90,7 +90,7 @@ class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
"""Return True if entity is available.""" """Return True if entity is available."""
return ( return (
super().available super().available
and DATA_KEY_OS in self.coordinator.data and DATA_KEY_SUPERVISOR in self.coordinator.data
and self.entity_description.key and self.entity_description.key
in self.coordinator.data[DATA_KEY_SUPERVISOR] in self.coordinator.data[DATA_KEY_SUPERVISOR]
) )

View File

@ -312,7 +312,6 @@ class Dishwasher(
"""Dishwasher class.""" """Dishwasher class."""
PROGRAMS = [ PROGRAMS = [
{"name": "Dishcare.Dishwasher.Program.PreRinse"},
{"name": "Dishcare.Dishwasher.Program.Auto1"}, {"name": "Dishcare.Dishwasher.Program.Auto1"},
{"name": "Dishcare.Dishwasher.Program.Auto2"}, {"name": "Dishcare.Dishwasher.Program.Auto2"},
{"name": "Dishcare.Dishwasher.Program.Auto3"}, {"name": "Dishcare.Dishwasher.Program.Auto3"},

View File

@ -63,7 +63,7 @@ class HomeKitSmokeSensor(HomeKitEntity, BinarySensorEntity):
class HomeKitCarbonMonoxideSensor(HomeKitEntity, BinarySensorEntity): class HomeKitCarbonMonoxideSensor(HomeKitEntity, BinarySensorEntity):
"""Representation of a Homekit BO sensor.""" """Representation of a Homekit BO sensor."""
_attr_device_class = BinarySensorDeviceClass.GAS _attr_device_class = BinarySensorDeviceClass.CO
def get_characteristic_types(self) -> list[str]: def get_characteristic_types(self) -> list[str]:
"""Define the homekit characteristics the entity is tracking.""" """Define the homekit characteristics the entity is tracking."""

View File

@ -293,7 +293,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured(updates=updated_ip_port) self._abort_if_unique_id_configured(updates=updated_ip_port)
for progress in self._async_in_progress(include_uninitialized=True): for progress in self._async_in_progress(include_uninitialized=True):
if progress["context"].get("unique_id") == normalized_hkid: context = progress["context"]
if context.get("unique_id") == normalized_hkid and not context.get(
"pairing"
):
if paired: if paired:
# If the device gets paired, we want to dismiss # If the device gets paired, we want to dismiss
# an existing discovery since we can no longer # an existing discovery since we can no longer
@ -350,6 +353,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
await self._async_setup_controller() await self._async_setup_controller()
if pair_info and self.finish_pairing: if pair_info and self.finish_pairing:
self.context["pairing"] = True
code = pair_info["pairing_code"] code = pair_info["pairing_code"]
try: try:
code = ensure_pin_format( code = ensure_pin_format(

View File

@ -1,4 +1,6 @@
"""Provides the constants needed for component.""" """Provides the constants needed for component."""
from enum import IntEnum
# How long our auth signature on the content should be valid for # How long our auth signature on the content should be valid for
CONTENT_AUTH_EXPIRY_TIME = 3600 * 24 CONTENT_AUTH_EXPIRY_TIME = 3600 * 24
@ -90,6 +92,32 @@ REPEAT_MODE_OFF = "off"
REPEAT_MODE_ONE = "one" REPEAT_MODE_ONE = "one"
REPEAT_MODES = [REPEAT_MODE_OFF, REPEAT_MODE_ALL, REPEAT_MODE_ONE] REPEAT_MODES = [REPEAT_MODE_OFF, REPEAT_MODE_ALL, REPEAT_MODE_ONE]
class MediaPlayerEntityFeature(IntEnum):
"""Supported features of the media player entity."""
PAUSE = 1
SEEK = 2
VOLUME_SET = 4
VOLUME_MUTE = 8
PREVIOUS_TRACK = 16
NEXT_TRACK = 32
TURN_ON = 128
TURN_OFF = 256
PLAY_MEDIA = 512
VOLUME_STEP = 1024
SELECT_SOURCE = 2048
STOP = 4096
CLEAR_PLAYLIST = 8192
PLAY = 16384
SHUFFLE_SET = 32768
SELECT_SOUND_MODE = 65536
BROWSE_MEDIA = 131072
REPEAT_SET = 262144
GROUPING = 524288
SUPPORT_PAUSE = 1 SUPPORT_PAUSE = 1
SUPPORT_SEEK = 2 SUPPORT_SEEK = 2
SUPPORT_VOLUME_SET = 4 SUPPORT_VOLUME_SET = 4

View File

@ -6,6 +6,7 @@ from collections.abc import Iterable
from typing import Any from typing import Any
from homeassistant.const import ( from homeassistant.const import (
ATTR_SUPPORTED_FEATURES,
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_STOP, SERVICE_MEDIA_STOP,
@ -33,6 +34,7 @@ from .const import (
SERVICE_PLAY_MEDIA, SERVICE_PLAY_MEDIA,
SERVICE_SELECT_SOUND_MODE, SERVICE_SELECT_SOUND_MODE,
SERVICE_SELECT_SOURCE, SERVICE_SELECT_SOURCE,
MediaPlayerEntityFeature,
) )
# mypy: allow-untyped-defs # mypy: allow-untyped-defs
@ -46,6 +48,8 @@ async def _async_reproduce_states(
reproduce_options: dict[str, Any] | None = None, reproduce_options: dict[str, Any] | None = None,
) -> None: ) -> None:
"""Reproduce component states.""" """Reproduce component states."""
cur_state = hass.states.get(state.entity_id)
features = cur_state.attributes[ATTR_SUPPORTED_FEATURES] if cur_state else 0
async def call_service(service: str, keys: Iterable) -> None: async def call_service(service: str, keys: Iterable) -> None:
"""Call service with set of attributes given.""" """Call service with set of attributes given."""
@ -59,28 +63,48 @@ async def _async_reproduce_states(
) )
if state.state == STATE_OFF: if state.state == STATE_OFF:
await call_service(SERVICE_TURN_OFF, []) if features & MediaPlayerEntityFeature.TURN_OFF:
await call_service(SERVICE_TURN_OFF, [])
# entities that are off have no other attributes to restore # entities that are off have no other attributes to restore
return return
if state.state in ( if (
STATE_ON, state.state
STATE_PLAYING, in (
STATE_IDLE, STATE_ON,
STATE_PAUSED, STATE_PLAYING,
STATE_IDLE,
STATE_PAUSED,
)
and features & MediaPlayerEntityFeature.TURN_ON
): ):
await call_service(SERVICE_TURN_ON, []) await call_service(SERVICE_TURN_ON, [])
if ATTR_MEDIA_VOLUME_LEVEL in state.attributes: cur_state = hass.states.get(state.entity_id)
features = cur_state.attributes[ATTR_SUPPORTED_FEATURES] if cur_state else 0
if (
ATTR_MEDIA_VOLUME_LEVEL in state.attributes
and features & MediaPlayerEntityFeature.VOLUME_SET
):
await call_service(SERVICE_VOLUME_SET, [ATTR_MEDIA_VOLUME_LEVEL]) await call_service(SERVICE_VOLUME_SET, [ATTR_MEDIA_VOLUME_LEVEL])
if ATTR_MEDIA_VOLUME_MUTED in state.attributes: if (
ATTR_MEDIA_VOLUME_MUTED in state.attributes
and features & MediaPlayerEntityFeature.VOLUME_MUTE
):
await call_service(SERVICE_VOLUME_MUTE, [ATTR_MEDIA_VOLUME_MUTED]) await call_service(SERVICE_VOLUME_MUTE, [ATTR_MEDIA_VOLUME_MUTED])
if ATTR_INPUT_SOURCE in state.attributes: if (
ATTR_INPUT_SOURCE in state.attributes
and features & MediaPlayerEntityFeature.SELECT_SOURCE
):
await call_service(SERVICE_SELECT_SOURCE, [ATTR_INPUT_SOURCE]) await call_service(SERVICE_SELECT_SOURCE, [ATTR_INPUT_SOURCE])
if ATTR_SOUND_MODE in state.attributes: if (
ATTR_SOUND_MODE in state.attributes
and features & MediaPlayerEntityFeature.SELECT_SOUND_MODE
):
await call_service(SERVICE_SELECT_SOUND_MODE, [ATTR_SOUND_MODE]) await call_service(SERVICE_SELECT_SOUND_MODE, [ATTR_SOUND_MODE])
already_playing = False already_playing = False
@ -88,18 +112,25 @@ async def _async_reproduce_states(
if (ATTR_MEDIA_CONTENT_TYPE in state.attributes) and ( if (ATTR_MEDIA_CONTENT_TYPE in state.attributes) and (
ATTR_MEDIA_CONTENT_ID in state.attributes ATTR_MEDIA_CONTENT_ID in state.attributes
): ):
await call_service( if features & MediaPlayerEntityFeature.PLAY_MEDIA:
SERVICE_PLAY_MEDIA, await call_service(
[ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_ENQUEUE], SERVICE_PLAY_MEDIA,
) [ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_ENQUEUE],
)
already_playing = True already_playing = True
if state.state == STATE_PLAYING and not already_playing: if (
not already_playing
and state.state == STATE_PLAYING
and features & MediaPlayerEntityFeature.PLAY
):
await call_service(SERVICE_MEDIA_PLAY, []) await call_service(SERVICE_MEDIA_PLAY, [])
elif state.state == STATE_IDLE: elif state.state == STATE_IDLE:
await call_service(SERVICE_MEDIA_STOP, []) if features & MediaPlayerEntityFeature.STOP:
await call_service(SERVICE_MEDIA_STOP, [])
elif state.state == STATE_PAUSED: elif state.state == STATE_PAUSED:
await call_service(SERVICE_MEDIA_PAUSE, []) if features & MediaPlayerEntityFeature.PAUSE:
await call_service(SERVICE_MEDIA_PAUSE, [])
async def async_reproduce_states( async def async_reproduce_states(

View File

@ -2,7 +2,7 @@
"domain": "openhome", "domain": "openhome",
"name": "Linn / OpenHome", "name": "Linn / OpenHome",
"documentation": "https://www.home-assistant.io/integrations/openhome", "documentation": "https://www.home-assistant.io/integrations/openhome",
"requirements": ["openhomedevice==2.0.1"], "requirements": ["openhomedevice==2.0.2"],
"codeowners": ["@bazwilliams"], "codeowners": ["@bazwilliams"],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["async_upnp_client", "openhomedevice"] "loggers": ["async_upnp_client", "openhomedevice"]

View File

@ -51,6 +51,7 @@ class ProsegurAlarm(alarm.AlarmControlPanelEntity):
self.contract = contract self.contract = contract
self._auth = auth self._auth = auth
self._attr_code_arm_required = False
self._attr_name = f"contract {self.contract}" self._attr_name = f"contract {self.contract}"
self._attr_unique_id = self.contract self._attr_unique_id = self.contract
self._attr_supported_features = SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME self._attr_supported_features = SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME

View File

@ -224,9 +224,7 @@ class SamsungTVDevice(MediaPlayerEntity):
startup_tasks.append(self._async_startup_app_list()) startup_tasks.append(self._async_startup_app_list())
if self._dmr_device and not self._dmr_device.is_subscribed: if self._dmr_device and not self._dmr_device.is_subscribed:
startup_tasks.append( startup_tasks.append(self._async_resubscribe_dmr())
self._dmr_device.async_subscribe_services(auto_resubscribe=True)
)
if not self._dmr_device and self._ssdp_rendering_control_location: if not self._dmr_device and self._ssdp_rendering_control_location:
startup_tasks.append(self._async_startup_dmr()) startup_tasks.append(self._async_startup_dmr())
@ -284,7 +282,7 @@ class SamsungTVDevice(MediaPlayerEntity):
# NETWORK,NONE # NETWORK,NONE
upnp_factory = UpnpFactory(upnp_requester, non_strict=True) upnp_factory = UpnpFactory(upnp_requester, non_strict=True)
upnp_device: UpnpDevice | None = None upnp_device: UpnpDevice | None = None
with contextlib.suppress(UpnpConnectionError): with contextlib.suppress(UpnpConnectionError, UpnpResponseError):
upnp_device = await upnp_factory.async_create_device( upnp_device = await upnp_factory.async_create_device(
self._ssdp_rendering_control_location self._ssdp_rendering_control_location
) )
@ -319,6 +317,11 @@ class SamsungTVDevice(MediaPlayerEntity):
LOGGER.debug("Error while subscribing during device connect: %r", err) LOGGER.debug("Error while subscribing during device connect: %r", err)
raise raise
async def _async_resubscribe_dmr(self) -> None:
assert self._dmr_device
with contextlib.suppress(UpnpConnectionError):
await self._dmr_device.async_subscribe_services(auto_resubscribe=True)
async def _async_shutdown_dmr(self) -> None: async def _async_shutdown_dmr(self) -> None:
"""Handle removal.""" """Handle removal."""
if (dmr_device := self._dmr_device) is not None: if (dmr_device := self._dmr_device) is not None:

View File

@ -2,7 +2,7 @@
"domain": "stream", "domain": "stream",
"name": "Stream", "name": "Stream",
"documentation": "https://www.home-assistant.io/integrations/stream", "documentation": "https://www.home-assistant.io/integrations/stream",
"requirements": ["PyTurboJPEG==1.6.6", "av==8.1.0"], "requirements": ["PyTurboJPEG==1.6.6", "ha-av==9.1.1-3"],
"dependencies": ["http"], "dependencies": ["http"],
"codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"],
"quality_scale": "internal", "quality_scale": "internal",

View File

@ -202,6 +202,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data[CONF_API_KEY], entry.data[CONF_API_KEY],
entry.data[CONF_LOCATION][CONF_LATITUDE], entry.data[CONF_LOCATION][CONF_LATITUDE],
entry.data[CONF_LOCATION][CONF_LONGITUDE], entry.data[CONF_LOCATION][CONF_LONGITUDE],
unit_system="metric",
session=async_get_clientsession(hass), session=async_get_clientsession(hass),
) )

View File

@ -31,16 +31,14 @@ from homeassistant.const import (
LENGTH_MILES, LENGTH_MILES,
PERCENTAGE, PERCENTAGE,
PRESSURE_HPA, PRESSURE_HPA,
PRESSURE_INHG,
SPEED_METERS_PER_SECOND, SPEED_METERS_PER_SECOND,
SPEED_MILES_PER_HOUR, SPEED_MILES_PER_HOUR,
TEMP_FAHRENHEIT, TEMP_CELSIUS,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify from homeassistant.util import slugify
from homeassistant.util.distance import convert as distance_convert from homeassistant.util.distance import convert as distance_convert
from homeassistant.util.pressure import convert as pressure_convert
from . import TomorrowioDataUpdateCoordinator, TomorrowioEntity from . import TomorrowioDataUpdateCoordinator, TomorrowioEntity
from .const import ( from .const import (
@ -80,7 +78,7 @@ class TomorrowioSensorEntityDescription(SensorEntityDescription):
unit_imperial: str | None = None unit_imperial: str | None = None
unit_metric: str | None = None unit_metric: str | None = None
multiplication_factor: Callable[[float], float] | float | None = None multiplication_factor: Callable[[float], float] | float | None = None
metric_conversion: Callable[[float], float] | float | None = None imperial_conversion: Callable[[float], float] | float | None = None
value_map: Any | None = None value_map: Any | None = None
def __post_init__(self) -> None: def __post_init__(self) -> None:
@ -105,13 +103,13 @@ SENSOR_TYPES = (
TomorrowioSensorEntityDescription( TomorrowioSensorEntityDescription(
key=TMRW_ATTR_FEELS_LIKE, key=TMRW_ATTR_FEELS_LIKE,
name="Feels Like", name="Feels Like",
native_unit_of_measurement=TEMP_FAHRENHEIT, native_unit_of_measurement=TEMP_CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
), ),
TomorrowioSensorEntityDescription( TomorrowioSensorEntityDescription(
key=TMRW_ATTR_DEW_POINT, key=TMRW_ATTR_DEW_POINT,
name="Dew Point", name="Dew Point",
native_unit_of_measurement=TEMP_FAHRENHEIT, native_unit_of_measurement=TEMP_CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
), ),
# Data comes in as inHg # Data comes in as inHg
@ -119,9 +117,6 @@ SENSOR_TYPES = (
key=TMRW_ATTR_PRESSURE_SURFACE_LEVEL, key=TMRW_ATTR_PRESSURE_SURFACE_LEVEL,
name="Pressure (Surface Level)", name="Pressure (Surface Level)",
native_unit_of_measurement=PRESSURE_HPA, native_unit_of_measurement=PRESSURE_HPA,
multiplication_factor=lambda val: pressure_convert(
val, PRESSURE_INHG, PRESSURE_HPA
),
device_class=SensorDeviceClass.PRESSURE, device_class=SensorDeviceClass.PRESSURE,
), ),
# Data comes in as BTUs/(hr * ft^2) # Data comes in as BTUs/(hr * ft^2)
@ -131,7 +126,7 @@ SENSOR_TYPES = (
name="Global Horizontal Irradiance", name="Global Horizontal Irradiance",
unit_imperial=IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, unit_imperial=IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT,
unit_metric=IRRADIATION_WATTS_PER_SQUARE_METER, unit_metric=IRRADIATION_WATTS_PER_SQUARE_METER,
metric_conversion=3.15459, imperial_conversion=(1 / 3.15459),
), ),
# Data comes in as miles # Data comes in as miles
TomorrowioSensorEntityDescription( TomorrowioSensorEntityDescription(
@ -139,8 +134,8 @@ SENSOR_TYPES = (
name="Cloud Base", name="Cloud Base",
unit_imperial=LENGTH_MILES, unit_imperial=LENGTH_MILES,
unit_metric=LENGTH_KILOMETERS, unit_metric=LENGTH_KILOMETERS,
metric_conversion=lambda val: distance_convert( imperial_conversion=lambda val: distance_convert(
val, LENGTH_MILES, LENGTH_KILOMETERS val, LENGTH_KILOMETERS, LENGTH_MILES
), ),
), ),
# Data comes in as miles # Data comes in as miles
@ -149,8 +144,8 @@ SENSOR_TYPES = (
name="Cloud Ceiling", name="Cloud Ceiling",
unit_imperial=LENGTH_MILES, unit_imperial=LENGTH_MILES,
unit_metric=LENGTH_KILOMETERS, unit_metric=LENGTH_KILOMETERS,
metric_conversion=lambda val: distance_convert( imperial_conversion=lambda val: distance_convert(
val, LENGTH_MILES, LENGTH_KILOMETERS val, LENGTH_KILOMETERS, LENGTH_MILES
), ),
), ),
TomorrowioSensorEntityDescription( TomorrowioSensorEntityDescription(
@ -164,8 +159,10 @@ SENSOR_TYPES = (
name="Wind Gust", name="Wind Gust",
unit_imperial=SPEED_MILES_PER_HOUR, unit_imperial=SPEED_MILES_PER_HOUR,
unit_metric=SPEED_METERS_PER_SECOND, unit_metric=SPEED_METERS_PER_SECOND,
metric_conversion=lambda val: distance_convert(val, LENGTH_MILES, LENGTH_METERS) imperial_conversion=lambda val: distance_convert(
/ 3600, val, LENGTH_METERS, LENGTH_MILES
)
* 3600,
), ),
TomorrowioSensorEntityDescription( TomorrowioSensorEntityDescription(
key=TMRW_ATTR_PRECIPITATION_TYPE, key=TMRW_ATTR_PRECIPITATION_TYPE,
@ -183,20 +180,16 @@ SENSOR_TYPES = (
multiplication_factor=convert_ppb_to_ugm3(48), multiplication_factor=convert_ppb_to_ugm3(48),
device_class=SensorDeviceClass.OZONE, device_class=SensorDeviceClass.OZONE,
), ),
# Data comes in as ug/ft^3
TomorrowioSensorEntityDescription( TomorrowioSensorEntityDescription(
key=TMRW_ATTR_PARTICULATE_MATTER_25, key=TMRW_ATTR_PARTICULATE_MATTER_25,
name="Particulate Matter < 2.5 μm", name="Particulate Matter < 2.5 μm",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
multiplication_factor=3.2808399**3,
device_class=SensorDeviceClass.PM25, device_class=SensorDeviceClass.PM25,
), ),
# Data comes in as ug/ft^3
TomorrowioSensorEntityDescription( TomorrowioSensorEntityDescription(
key=TMRW_ATTR_PARTICULATE_MATTER_10, key=TMRW_ATTR_PARTICULATE_MATTER_10,
name="Particulate Matter < 10 μm", name="Particulate Matter < 10 μm",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
multiplication_factor=3.2808399**3,
device_class=SensorDeviceClass.PM10, device_class=SensorDeviceClass.PM10,
), ),
# Data comes in as ppb # Data comes in as ppb
@ -360,15 +353,15 @@ class BaseTomorrowioSensorEntity(TomorrowioEntity, SensorEntity):
if desc.multiplication_factor is not None: if desc.multiplication_factor is not None:
state = handle_conversion(state, desc.multiplication_factor) state = handle_conversion(state, desc.multiplication_factor)
# If an imperial unit isn't provided, we always want to convert to metric since # If there is an imperial conversion needed and the instance is using imperial,
# that is what the UI expects # apply the conversion logic.
if ( if (
desc.metric_conversion desc.imperial_conversion
and desc.unit_imperial is not None and desc.unit_imperial is not None
and desc.unit_imperial != desc.unit_metric and desc.unit_imperial != desc.unit_metric
and self.hass.config.units.is_metric and not self.hass.config.units.is_metric
): ):
return handle_conversion(state, desc.metric_conversion) return handle_conversion(state, desc.imperial_conversion)
return state return state

View File

@ -29,6 +29,7 @@ KEYS_TO_REDACT = {
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
"network_key", "network_key",
CONF_NWK_EXTENDED_PAN_ID, CONF_NWK_EXTENDED_PAN_ID,
"partner_ieee",
} }
@ -50,7 +51,7 @@ async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> dict: ) -> dict:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
config: dict = hass.data[DATA_ZHA][DATA_ZHA_CONFIG] config: dict = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {})
gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
return async_redact_data( return async_redact_data(
{ {

View File

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

View File

@ -347,10 +347,6 @@ auroranoaa==0.0.2
# homeassistant.components.aurora_abb_powerone # homeassistant.components.aurora_abb_powerone
aurorapy==0.2.6 aurorapy==0.2.6
# homeassistant.components.generic
# homeassistant.components.stream
av==8.1.0
# homeassistant.components.avea # homeassistant.components.avea
# avea==1.5.1 # avea==1.5.1
@ -766,6 +762,10 @@ gstreamer-player==1.1.2
# homeassistant.components.profiler # homeassistant.components.profiler
guppy3==3.1.2 guppy3==3.1.2
# homeassistant.components.generic
# homeassistant.components.stream
ha-av==9.1.1-3
# homeassistant.components.ffmpeg # homeassistant.components.ffmpeg
ha-ffmpeg==3.0.2 ha-ffmpeg==3.0.2
@ -1141,7 +1141,7 @@ openerz-api==0.1.0
openevsewifi==1.1.0 openevsewifi==1.1.0
# homeassistant.components.openhome # homeassistant.components.openhome
openhomedevice==2.0.1 openhomedevice==2.0.2
# homeassistant.components.opensensemap # homeassistant.components.opensensemap
opensensemap-api==0.2.0 opensensemap-api==0.2.0

View File

@ -277,10 +277,6 @@ auroranoaa==0.0.2
# homeassistant.components.aurora_abb_powerone # homeassistant.components.aurora_abb_powerone
aurorapy==0.2.6 aurorapy==0.2.6
# homeassistant.components.generic
# homeassistant.components.stream
av==8.1.0
# homeassistant.components.axis # homeassistant.components.axis
axis==44 axis==44
@ -536,6 +532,10 @@ growattServer==1.1.0
# homeassistant.components.profiler # homeassistant.components.profiler
guppy3==3.1.2 guppy3==3.1.2
# homeassistant.components.generic
# homeassistant.components.stream
ha-av==9.1.1-3
# homeassistant.components.ffmpeg # homeassistant.components.ffmpeg
ha-ffmpeg==3.0.2 ha-ffmpeg==3.0.2

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
name = homeassistant name = homeassistant
version = 2022.4.3 version = 2022.4.4
author = The Home Assistant Authors author = The Home Assistant Authors
author_email = hello@home-assistant.io author_email = hello@home-assistant.io
license = Apache-2.0 license = Apache-2.0

View File

@ -388,7 +388,7 @@ async def test_ssdp_flow_upnp_udn(
async def test_ssdp_missing_services(hass: HomeAssistant) -> None: async def test_ssdp_missing_services(hass: HomeAssistant) -> None:
"""Test SSDP ignores devices that are missing required services.""" """Test SSDP ignores devices that are missing required services."""
# No services defined at all # No service list at all
discovery = dataclasses.replace(MOCK_DISCOVERY) discovery = dataclasses.replace(MOCK_DISCOVERY)
discovery.upnp = discovery.upnp.copy() discovery.upnp = discovery.upnp.copy()
del discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST] del discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST]
@ -400,6 +400,18 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None:
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "not_dmr" assert result["reason"] == "not_dmr"
# Service list does not contain services
discovery = dataclasses.replace(MOCK_DISCOVERY)
discovery.upnp = discovery.upnp.copy()
discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST] = {"bad_key": "bad_value"}
result = await hass.config_entries.flow.async_init(
DLNA_DOMAIN,
context={"source": config_entries.SOURCE_SSDP},
data=discovery,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "not_dmr"
# AVTransport service is missing # AVTransport service is missing
discovery = dataclasses.replace(MOCK_DISCOVERY) discovery = dataclasses.replace(MOCK_DISCOVERY)
discovery.upnp = discovery.upnp.copy() discovery.upnp = discovery.upnp.copy()
@ -417,6 +429,28 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None:
assert result["reason"] == "not_dmr" assert result["reason"] == "not_dmr"
async def test_ssdp_single_service(hass: HomeAssistant) -> None:
"""Test SSDP discovery info with only one service defined.
THe etree_to_dict function turns multiple services into a list of dicts, but
a single service into only a dict.
"""
discovery = dataclasses.replace(MOCK_DISCOVERY)
discovery.upnp = discovery.upnp.copy()
service_list = discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST].copy()
# Turn mock's list of service dicts into a single dict
service_list["service"] = service_list["service"][0]
discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST] = service_list
result = await hass.config_entries.flow.async_init(
DLNA_DOMAIN,
context={"source": config_entries.SOURCE_SSDP},
data=discovery,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "not_dmr"
async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: async def test_ssdp_ignore_device(hass: HomeAssistant) -> None:
"""Test SSDP discovery ignores certain devices.""" """Test SSDP discovery ignores certain devices."""
discovery = dataclasses.replace(MOCK_DISCOVERY) discovery = dataclasses.replace(MOCK_DISCOVERY)

View File

@ -325,7 +325,7 @@ async def test_ssdp_flow_upnp_udn(
async def test_ssdp_missing_services(hass: HomeAssistant) -> None: async def test_ssdp_missing_services(hass: HomeAssistant) -> None:
"""Test SSDP ignores devices that are missing required services.""" """Test SSDP ignores devices that are missing required services."""
# No services defined at all # No service list at all
discovery = dataclasses.replace(MOCK_DISCOVERY) discovery = dataclasses.replace(MOCK_DISCOVERY)
discovery.upnp = dict(discovery.upnp) discovery.upnp = dict(discovery.upnp)
del discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST] del discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST]
@ -337,6 +337,18 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None:
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "not_dms" assert result["reason"] == "not_dms"
# Service list does not contain services
discovery = dataclasses.replace(MOCK_DISCOVERY)
discovery.upnp = dict(discovery.upnp)
discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST] = {"bad_key": "bad_value"}
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_SSDP},
data=discovery,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "not_dms"
# ContentDirectory service is missing # ContentDirectory service is missing
discovery = dataclasses.replace(MOCK_DISCOVERY) discovery = dataclasses.replace(MOCK_DISCOVERY)
discovery.upnp = dict(discovery.upnp) discovery.upnp = dict(discovery.upnp)
@ -352,3 +364,25 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None:
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "not_dms" assert result["reason"] == "not_dms"
async def test_ssdp_single_service(hass: HomeAssistant) -> None:
"""Test SSDP discovery info with only one service defined.
THe etree_to_dict function turns multiple services into a list of dicts, but
a single service into only a dict.
"""
discovery = dataclasses.replace(MOCK_DISCOVERY)
discovery.upnp = dict(discovery.upnp)
service_list = dict(discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST])
# Turn mock's list of service dicts into a single dict
service_list["service"] = service_list["service"][0]
discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST] = service_list
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_SSDP},
data=discovery,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "not_dms"

View File

@ -114,7 +114,7 @@ async def test_carbon_monoxide_sensor_read_state(hass, utcnow):
state = await helper.poll_and_get_state() state = await helper.poll_and_get_state()
assert state.state == "on" assert state.state == "on"
assert state.attributes["device_class"] == BinarySensorDeviceClass.GAS assert state.attributes["device_class"] == BinarySensorDeviceClass.CO
def create_occupancy_sensor_service(accessory): def create_occupancy_sensor_service(accessory):

View File

@ -1,4 +1,5 @@
"""Tests for homekit_controller config flow.""" """Tests for homekit_controller config flow."""
import asyncio
from unittest import mock from unittest import mock
import unittest.mock import unittest.mock
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
@ -14,6 +15,7 @@ from homeassistant import config_entries
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller import config_flow
from homeassistant.components.homekit_controller.const import KNOWN_DEVICES from homeassistant.components.homekit_controller.const import KNOWN_DEVICES
from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY
from homeassistant.helpers import device_registry from homeassistant.helpers import device_registry
from tests.common import MockConfigEntry, mock_device_registry from tests.common import MockConfigEntry, mock_device_registry
@ -133,7 +135,7 @@ def get_flow_context(hass, result):
def get_device_discovery_info( def get_device_discovery_info(
device, upper_case_props=False, missing_csharp=False device, upper_case_props=False, missing_csharp=False, paired=False
) -> zeroconf.ZeroconfServiceInfo: ) -> zeroconf.ZeroconfServiceInfo:
"""Turn a aiohomekit format zeroconf entry into a homeassistant one.""" """Turn a aiohomekit format zeroconf entry into a homeassistant one."""
result = zeroconf.ZeroconfServiceInfo( result = zeroconf.ZeroconfServiceInfo(
@ -150,7 +152,7 @@ def get_device_discovery_info(
"s#": device.description.state_num, "s#": device.description.state_num,
"ff": "0", "ff": "0",
"ci": "0", "ci": "0",
"sf": "1", "sf": "0" if paired else "1",
"sh": "", "sh": "",
}, },
type="_hap._tcp.local.", type="_hap._tcp.local.",
@ -250,10 +252,8 @@ async def test_abort_duplicate_flow(hass, controller):
async def test_pair_already_paired_1(hass, controller): async def test_pair_already_paired_1(hass, controller):
"""Already paired.""" """Already paired."""
device = setup_mock_accessory(controller) device = setup_mock_accessory(controller)
discovery_info = get_device_discovery_info(device)
# Flag device as already paired # Flag device as already paired
discovery_info.properties["sf"] = 0x0 discovery_info = get_device_discovery_info(device, paired=True)
# Device is discovered # Device is discovered
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -692,6 +692,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected)
"title_placeholders": {"name": "TestDevice"}, "title_placeholders": {"name": "TestDevice"},
"unique_id": "00:00:00:00:00:00", "unique_id": "00:00:00:00:00:00",
"source": config_entries.SOURCE_ZEROCONF, "source": config_entries.SOURCE_ZEROCONF,
"pairing": True,
} }
@ -883,3 +884,69 @@ async def test_discovery_dismiss_existing_flow_on_paired(hass, controller):
len(hass.config_entries.flow.async_progress_by_handler("homekit_controller")) len(hass.config_entries.flow.async_progress_by_handler("homekit_controller"))
== 0 == 0
) )
async def test_mdns_update_to_paired_during_pairing(hass, controller):
"""Test we do not abort pairing if mdns is updated to reflect paired during pairing."""
device = setup_mock_accessory(controller)
discovery_info = get_device_discovery_info(device)
discovery_info_paired = get_device_discovery_info(device, paired=True)
# Device is discovered
result = await hass.config_entries.flow.async_init(
"homekit_controller",
context={"source": config_entries.SOURCE_ZEROCONF},
data=discovery_info,
)
assert get_flow_context(hass, result) == {
"title_placeholders": {"name": "TestDevice"},
"unique_id": "00:00:00:00:00:00",
"source": config_entries.SOURCE_ZEROCONF,
}
mdns_update_to_paired = asyncio.Event()
original_async_start_pairing = device.async_start_pairing
async def _async_start_pairing(*args, **kwargs):
finish_pairing = await original_async_start_pairing(*args, **kwargs)
async def _finish_pairing(*args, **kwargs):
# Insert an event wait to make sure
# we trigger the mdns update in the middle of the pairing
await mdns_update_to_paired.wait()
return await finish_pairing(*args, **kwargs)
return _finish_pairing
with patch.object(device, "async_start_pairing", _async_start_pairing):
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == "form"
assert get_flow_context(hass, result) == {
"title_placeholders": {"name": "TestDevice"},
"unique_id": "00:00:00:00:00:00",
"source": config_entries.SOURCE_ZEROCONF,
}
# User enters pairing code
task = asyncio.create_task(
hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"pairing_code": "111-22-333"}
)
)
# Make sure when the device is discovered as paired via mdns
# it does not abort pairing if it happens before pairing is finished
result2 = await hass.config_entries.flow.async_init(
"homekit_controller",
context={"source": config_entries.SOURCE_ZEROCONF},
data=discovery_info_paired,
)
assert result2["type"] == RESULT_TYPE_ABORT
assert result2["reason"] == "already_paired"
mdns_update_to_paired.set()
result = await task
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Koogeek-LS1-20833F"
assert result["data"] == {}

View File

@ -14,9 +14,11 @@ from homeassistant.components.media_player.const import (
SERVICE_PLAY_MEDIA, SERVICE_PLAY_MEDIA,
SERVICE_SELECT_SOUND_MODE, SERVICE_SELECT_SOUND_MODE,
SERVICE_SELECT_SOURCE, SERVICE_SELECT_SOURCE,
MediaPlayerEntityFeature,
) )
from homeassistant.components.media_player.reproduce_state import async_reproduce_states from homeassistant.components.media_player.reproduce_state import async_reproduce_states
from homeassistant.const import ( from homeassistant.const import (
ATTR_SUPPORTED_FEATURES,
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_STOP, SERVICE_MEDIA_STOP,
@ -39,31 +41,47 @@ ENTITY_2 = "media_player.test2"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"service,state", "service,state,supported_feature",
[ [
(SERVICE_TURN_ON, STATE_ON), (SERVICE_TURN_ON, STATE_ON, MediaPlayerEntityFeature.TURN_ON),
(SERVICE_TURN_OFF, STATE_OFF), (SERVICE_TURN_OFF, STATE_OFF, MediaPlayerEntityFeature.TURN_OFF),
(SERVICE_MEDIA_PLAY, STATE_PLAYING), (SERVICE_MEDIA_PLAY, STATE_PLAYING, MediaPlayerEntityFeature.PLAY),
(SERVICE_MEDIA_STOP, STATE_IDLE), (SERVICE_MEDIA_STOP, STATE_IDLE, MediaPlayerEntityFeature.STOP),
(SERVICE_MEDIA_PAUSE, STATE_PAUSED), (SERVICE_MEDIA_PAUSE, STATE_PAUSED, MediaPlayerEntityFeature.PAUSE),
], ],
) )
async def test_state(hass, service, state): async def test_state(hass, service, state, supported_feature):
"""Test that we can turn a state into a service call.""" """Test that we can turn a state into a service call."""
calls_1 = async_mock_service(hass, DOMAIN, service) calls_1 = async_mock_service(hass, DOMAIN, service)
if service != SERVICE_TURN_ON: if service != SERVICE_TURN_ON:
async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
# Don't support the feature won't call the service
hass.states.async_set(ENTITY_1, "something", {ATTR_SUPPORTED_FEATURES: 0})
await async_reproduce_states(hass, [State(ENTITY_1, state)]) await async_reproduce_states(hass, [State(ENTITY_1, state)])
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(calls_1) == 0
hass.states.async_set(
ENTITY_1, "something", {ATTR_SUPPORTED_FEATURES: supported_feature}
)
await async_reproduce_states(hass, [State(ENTITY_1, state)])
assert len(calls_1) == 1 assert len(calls_1) == 1
assert calls_1[0].data == {"entity_id": ENTITY_1} assert calls_1[0].data == {"entity_id": ENTITY_1}
async def test_turn_on_with_mode(hass): async def test_turn_on_with_mode(hass):
"""Test that state with additional attributes call multiple services.""" """Test that state with additional attributes call multiple services."""
hass.states.async_set(
ENTITY_1,
"something",
{
ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.TURN_ON
| MediaPlayerEntityFeature.SELECT_SOUND_MODE
},
)
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
calls_2 = async_mock_service(hass, DOMAIN, SERVICE_SELECT_SOUND_MODE) calls_2 = async_mock_service(hass, DOMAIN, SERVICE_SELECT_SOUND_MODE)
@ -82,6 +100,13 @@ async def test_turn_on_with_mode(hass):
async def test_multiple_same_state(hass): async def test_multiple_same_state(hass):
"""Test that multiple states with same state gets calls.""" """Test that multiple states with same state gets calls."""
for entity in ENTITY_1, ENTITY_2:
hass.states.async_set(
entity,
"something",
{ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.TURN_ON},
)
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
await async_reproduce_states(hass, [State(ENTITY_1, "on"), State(ENTITY_2, "on")]) await async_reproduce_states(hass, [State(ENTITY_1, "on"), State(ENTITY_2, "on")])
@ -96,6 +121,16 @@ async def test_multiple_same_state(hass):
async def test_multiple_different_state(hass): async def test_multiple_different_state(hass):
"""Test that multiple states with different state gets calls.""" """Test that multiple states with different state gets calls."""
for entity in ENTITY_1, ENTITY_2:
hass.states.async_set(
entity,
"something",
{
ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.TURN_ON
| MediaPlayerEntityFeature.TURN_OFF
},
)
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
calls_2 = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) calls_2 = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF)
@ -111,6 +146,12 @@ async def test_multiple_different_state(hass):
async def test_state_with_context(hass): async def test_state_with_context(hass):
"""Test that context is forwarded.""" """Test that context is forwarded."""
hass.states.async_set(
ENTITY_1,
"something",
{ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.TURN_ON},
)
calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
context = Context() context = Context()
@ -126,6 +167,16 @@ async def test_state_with_context(hass):
async def test_attribute_no_state(hass): async def test_attribute_no_state(hass):
"""Test that no state service call is made with none state.""" """Test that no state service call is made with none state."""
hass.states.async_set(
ENTITY_1,
"something",
{
ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.TURN_ON
| MediaPlayerEntityFeature.TURN_OFF
| MediaPlayerEntityFeature.SELECT_SOUND_MODE
},
)
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) calls_1 = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON)
calls_2 = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) calls_2 = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF)
calls_3 = async_mock_service(hass, DOMAIN, SERVICE_SELECT_SOUND_MODE) calls_3 = async_mock_service(hass, DOMAIN, SERVICE_SELECT_SOUND_MODE)
@ -145,16 +196,38 @@ async def test_attribute_no_state(hass):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"service,attribute", "service,attribute,supported_feature",
[ [
(SERVICE_VOLUME_SET, ATTR_MEDIA_VOLUME_LEVEL), (
(SERVICE_VOLUME_MUTE, ATTR_MEDIA_VOLUME_MUTED), SERVICE_VOLUME_SET,
(SERVICE_SELECT_SOURCE, ATTR_INPUT_SOURCE), ATTR_MEDIA_VOLUME_LEVEL,
(SERVICE_SELECT_SOUND_MODE, ATTR_SOUND_MODE), MediaPlayerEntityFeature.VOLUME_SET,
),
(
SERVICE_VOLUME_MUTE,
ATTR_MEDIA_VOLUME_MUTED,
MediaPlayerEntityFeature.VOLUME_MUTE,
),
(
SERVICE_SELECT_SOURCE,
ATTR_INPUT_SOURCE,
MediaPlayerEntityFeature.SELECT_SOURCE,
),
(
SERVICE_SELECT_SOUND_MODE,
ATTR_SOUND_MODE,
MediaPlayerEntityFeature.SELECT_SOUND_MODE,
),
], ],
) )
async def test_attribute(hass, service, attribute): async def test_attribute(hass, service, attribute, supported_feature):
"""Test that service call is made for each attribute.""" """Test that service call is made for each attribute."""
hass.states.async_set(
ENTITY_1,
"something",
{ATTR_SUPPORTED_FEATURES: supported_feature},
)
calls_1 = async_mock_service(hass, DOMAIN, service) calls_1 = async_mock_service(hass, DOMAIN, service)
value = "dummy" value = "dummy"
@ -168,7 +241,12 @@ async def test_attribute(hass, service, attribute):
async def test_play_media(hass): async def test_play_media(hass):
"""Test that no state service call is made with none state.""" """Test playing media."""
hass.states.async_set(
ENTITY_1,
"something",
{ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.PLAY_MEDIA},
)
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_PLAY_MEDIA) calls_1 = async_mock_service(hass, DOMAIN, SERVICE_PLAY_MEDIA)
value_1 = "dummy_1" value_1 = "dummy_1"

View File

@ -146,8 +146,8 @@ async def test_v4_sensor(hass: HomeAssistant) -> None:
check_sensor_state(hass, CO, "0.0") check_sensor_state(hass, CO, "0.0")
check_sensor_state(hass, NO2, "20.08") check_sensor_state(hass, NO2, "20.08")
check_sensor_state(hass, SO2, "4.32") check_sensor_state(hass, SO2, "4.32")
check_sensor_state(hass, PM25, "5.3") check_sensor_state(hass, PM25, "0.15")
check_sensor_state(hass, PM10, "20.13") check_sensor_state(hass, PM10, "0.57")
check_sensor_state(hass, MEP_AQI, "23") check_sensor_state(hass, MEP_AQI, "23")
check_sensor_state(hass, MEP_HEALTH_CONCERN, "good") check_sensor_state(hass, MEP_HEALTH_CONCERN, "good")
check_sensor_state(hass, MEP_PRIMARY_POLLUTANT, "pm10") check_sensor_state(hass, MEP_PRIMARY_POLLUTANT, "pm10")
@ -158,14 +158,14 @@ async def test_v4_sensor(hass: HomeAssistant) -> None:
check_sensor_state(hass, GRASS_POLLEN, "none") check_sensor_state(hass, GRASS_POLLEN, "none")
check_sensor_state(hass, WEED_POLLEN, "none") check_sensor_state(hass, WEED_POLLEN, "none")
check_sensor_state(hass, TREE_POLLEN, "none") check_sensor_state(hass, TREE_POLLEN, "none")
check_sensor_state(hass, FEELS_LIKE, "38.5") check_sensor_state(hass, FEELS_LIKE, "101.3")
check_sensor_state(hass, DEW_POINT, "22.68") check_sensor_state(hass, DEW_POINT, "72.82")
check_sensor_state(hass, PRESSURE_SURFACE_LEVEL, "997.97") check_sensor_state(hass, PRESSURE_SURFACE_LEVEL, "29.47")
check_sensor_state(hass, GHI, "0.0") check_sensor_state(hass, GHI, "0")
check_sensor_state(hass, CLOUD_BASE, "1.19") check_sensor_state(hass, CLOUD_BASE, "0.74")
check_sensor_state(hass, CLOUD_COVER, "100") check_sensor_state(hass, CLOUD_COVER, "100")
check_sensor_state(hass, CLOUD_CEILING, "1.19") check_sensor_state(hass, CLOUD_CEILING, "0.74")
check_sensor_state(hass, WIND_GUST, "5.65") check_sensor_state(hass, WIND_GUST, "12.64")
check_sensor_state(hass, PRECIPITATION_TYPE, "rain") check_sensor_state(hass, PRECIPITATION_TYPE, "rain")