mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 06:07:17 +00:00
Merge pull request #70054 from home-assistant/rc
This commit is contained in:
commit
30db51a49c
@ -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",
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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]
|
||||||
)
|
)
|
||||||
|
@ -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"},
|
||||||
|
@ -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."""
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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"]
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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",
|
||||||
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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(
|
||||||
{
|
{
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
@ -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):
|
||||||
|
@ -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"] == {}
|
||||||
|
@ -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"
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user