forked from home-assistant/core
Compare commits
55 Commits
media-sele
...
schema_obj
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df8925b1b9 | ||
|
|
d61cf0f805 | ||
|
|
272837205c | ||
|
|
956f726ef3 | ||
|
|
fada81e1ce | ||
|
|
6a16424bb4 | ||
|
|
f90a740429 | ||
|
|
3dba7e5bd2 | ||
|
|
8d8ff011fc | ||
|
|
6befd065a1 | ||
|
|
9adf493acd | ||
|
|
a29d5fb56c | ||
|
|
bcb87cf812 | ||
|
|
d01758cea8 | ||
|
|
5487bfe1d9 | ||
|
|
fec65f40fc | ||
|
|
596951ea9f | ||
|
|
75d6b885cf | ||
|
|
3fad76dfa1 | ||
|
|
43d8a151ab | ||
|
|
07110e288d | ||
|
|
ba2aac4614 | ||
|
|
3449dae7a2 | ||
|
|
b8cd3f3635 | ||
|
|
be53ad5449 | ||
|
|
ffd940e07c | ||
|
|
5e31b5ac4f | ||
|
|
81257f9d57 | ||
|
|
ce1678719a | ||
|
|
fc6844b3c9 | ||
|
|
8e82e3aa3a | ||
|
|
3bc68941e6 | ||
|
|
e69b38ab2c | ||
|
|
ed9503324d | ||
|
|
22a06a6c2e | ||
|
|
3b611b9b03 | ||
|
|
79cc3bffc6 | ||
|
|
5c455304a5 | ||
|
|
058f860be7 | ||
|
|
ef319c966d | ||
|
|
adc4e9fdc1 | ||
|
|
40a00fb790 | ||
|
|
0926b16095 | ||
|
|
308c89af4a | ||
|
|
b0c2a47288 | ||
|
|
c446cce2cc | ||
|
|
e02267ad89 | ||
|
|
36381e6753 | ||
|
|
6533562f4e | ||
|
|
1bc6ea98ce | ||
|
|
bab34b844b | ||
|
|
ad3dac0373 | ||
|
|
c5d93e5456 | ||
|
|
ef9b46dce5 | ||
|
|
6f3ceb83c2 |
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -108,7 +108,7 @@ jobs:
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/intents-package
|
||||
repo: OHF-Voice/intents-package
|
||||
branch: main
|
||||
workflow: nightly.yaml
|
||||
workflow_conclusion: success
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.1.4"]
|
||||
"requirements": ["aioamazondevices==3.1.12"]
|
||||
}
|
||||
|
||||
@@ -105,11 +105,6 @@ DEFAULT_MAX_HUMIDITY = 99
|
||||
|
||||
CONVERTIBLE_ATTRIBUTE = [ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH]
|
||||
|
||||
# Can be removed in 2025.1 after deprecation period of the new feature flags
|
||||
CHECK_TURN_ON_OFF_FEATURE_FLAG = (
|
||||
ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
|
||||
)
|
||||
|
||||
SET_TEMPERATURE_SCHEMA = vol.All(
|
||||
cv.has_at_least_one_key(
|
||||
ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.101.0"],
|
||||
"requirements": ["hass-nabucasa==0.102.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -300,10 +300,6 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
def supported_features(self) -> CoverEntityFeature:
|
||||
"""Flag supported features."""
|
||||
if (features := self._attr_supported_features) is not None:
|
||||
if type(features) is int:
|
||||
new_features = CoverEntityFeature(features)
|
||||
self._report_deprecated_supported_features_values(new_features)
|
||||
return new_features
|
||||
return features
|
||||
|
||||
supported_features = (
|
||||
|
||||
@@ -91,7 +91,9 @@ async def async_unload_entry(
|
||||
|
||||
|
||||
async def async_remove_config_entry_device(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
|
||||
hass: HomeAssistant,
|
||||
config_entry: DevoloHomeControlConfigEntry,
|
||||
device_entry: DeviceEntry,
|
||||
) -> bool:
|
||||
"""Remove a config entry from a device."""
|
||||
return True
|
||||
|
||||
@@ -87,6 +87,7 @@ class DevoloScannerEntity( # pylint: disable=hass-enforce-class-module
|
||||
):
|
||||
"""Representation of a devolo device tracker."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "device_tracker"
|
||||
|
||||
def __init__(
|
||||
@@ -99,6 +100,7 @@ class DevoloScannerEntity( # pylint: disable=hass-enforce-class-module
|
||||
super().__init__(coordinator)
|
||||
self._device = device
|
||||
self._attr_mac_address = mac
|
||||
self._attr_name = mac
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, str]:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["discord"],
|
||||
"requirements": ["nextcord==2.6.0"]
|
||||
"requirements": ["nextcord==3.1.0"]
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ _VOICE_ASSISTANT_EVENT_TYPES: EsphomeEnumMapper[
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_STT_START: PipelineEventType.STT_START,
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_STT_END: PipelineEventType.STT_END,
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_START: PipelineEventType.INTENT_START,
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS: PipelineEventType.INTENT_PROGRESS,
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END: PipelineEventType.INTENT_END,
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_TTS_START: PipelineEventType.TTS_START,
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END: PipelineEventType.TTS_END,
|
||||
@@ -282,6 +283,12 @@ class EsphomeAssistSatellite(
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_STT_END:
|
||||
assert event.data is not None
|
||||
data_to_send = {"text": event.data["stt_output"]["text"]}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS:
|
||||
data_to_send = {
|
||||
"tts_start_streaming": bool(
|
||||
event.data and event.data.get("tts_start_streaming")
|
||||
),
|
||||
}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END:
|
||||
assert event.data is not None
|
||||
data_to_send = {
|
||||
@@ -332,7 +339,7 @@ class EsphomeAssistSatellite(
|
||||
}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_RUN_START:
|
||||
assert event.data is not None
|
||||
if tts_output := event.data["tts_output"]:
|
||||
if tts_output := event.data.get("tts_output"):
|
||||
path = tts_output["url"]
|
||||
url = async_process_play_media_url(self.hass, path)
|
||||
data_to_send = {"url": url}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==32.2.1",
|
||||
"aioesphomeapi==32.2.4",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.16.0"
|
||||
],
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.0"]
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.4"]
|
||||
}
|
||||
|
||||
@@ -133,8 +133,8 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
def _parse_routing_response(self, response: dict[str, Any]) -> HERETravelTimeData:
|
||||
"""Parse the routing response dict to a HERETravelTimeData."""
|
||||
distance: float = 0.0
|
||||
duration: float = 0.0
|
||||
duration_in_traffic: float = 0.0
|
||||
duration: int = 0
|
||||
duration_in_traffic: int = 0
|
||||
|
||||
for section in response["routes"][0]["sections"]:
|
||||
distance += DistanceConverter.convert(
|
||||
@@ -167,8 +167,8 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
destination_name = names[0]["value"]
|
||||
return HERETravelTimeData(
|
||||
attribution=None,
|
||||
duration=round(duration / 60),
|
||||
duration_in_traffic=round(duration_in_traffic / 60),
|
||||
duration=duration,
|
||||
duration_in_traffic=duration_in_traffic,
|
||||
distance=distance,
|
||||
origin=f"{mapped_origin_lat},{mapped_origin_lon}",
|
||||
destination=f"{mapped_destination_lat},{mapped_destination_lon}",
|
||||
@@ -271,13 +271,13 @@ class HERETransitDataUpdateCoordinator(
|
||||
UnitOfLength.METERS,
|
||||
UnitOfLength.KILOMETERS,
|
||||
)
|
||||
duration: float = sum(
|
||||
duration: int = sum(
|
||||
section["travelSummary"]["duration"] for section in sections
|
||||
)
|
||||
return HERETravelTimeData(
|
||||
attribution=attribution,
|
||||
duration=round(duration / 60),
|
||||
duration_in_traffic=round(duration / 60),
|
||||
duration=duration,
|
||||
duration_in_traffic=duration,
|
||||
distance=distance,
|
||||
origin=f"{mapped_origin_lat},{mapped_origin_lon}",
|
||||
destination=f"{mapped_destination_lat},{mapped_destination_lon}",
|
||||
|
||||
@@ -56,7 +56,8 @@ def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...]
|
||||
key=ATTR_DURATION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
suggested_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
translation_key="duration_in_traffic",
|
||||
@@ -64,7 +65,8 @@ def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...]
|
||||
key=ATTR_DURATION_IN_TRAFFIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
suggested_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
translation_key="distance",
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.74", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.75", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
"requirements": ["aiohomeconnect==0.17.1"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiohomeconnect==0.18.0"],
|
||||
"zeroconf": ["_homeconnect._tcp.local."]
|
||||
}
|
||||
|
||||
71
homeassistant/components/home_connect/quality_scale.yaml
Normal file
71
homeassistant/components/home_connect/quality_scale.yaml
Normal file
@@ -0,0 +1,71 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup: done
|
||||
appropriate-polling:
|
||||
status: done
|
||||
comment: |
|
||||
Full polling is performed at the configuration entry setup and
|
||||
device polling is performed when a CONNECTED or a PAIRED event is received.
|
||||
If many CONNECTED or PAIRED events are received for a device within a short time span,
|
||||
the integration will stop polling for that device and will create a repair issue.
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: done
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default:
|
||||
status: done
|
||||
comment: |
|
||||
Event entities are disabled by default to prevent user confusion regarding
|
||||
which events are supported by its appliance.
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration doesn't have settings in its configuration flow.
|
||||
repair-issues: done
|
||||
stale-devices: done
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
@@ -177,9 +177,9 @@
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"upper": "[%key;component::homee::entity::event::button_state::state_attributes::event_type::state::upper%]",
|
||||
"lower": "[%key;component::homee::entity::event::button_state::state_attributes::event_type::state::lower%]",
|
||||
"released": "[%key;component::homee::entity::event::button_state::state_attributes::event_type::state::released%]"
|
||||
"upper": "[%key:component::homee::entity::event::button_state::state_attributes::event_type::state::upper%]",
|
||||
"lower": "[%key:component::homee::entity::event::button_state::state_attributes::event_type::state::lower%]",
|
||||
"released": "[%key:component::homee::entity::event::button_state::state_attributes::event_type::state::released%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,7 @@
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"release": "[%key;component::homee::entity::event::button_state::state_attributes::event_type::state::released%]",
|
||||
"release": "[%key:component::homee::entity::event::button_state::state_attributes::event_type::state::released%]",
|
||||
"up": "Up",
|
||||
"down": "Down",
|
||||
"stop": "Stop",
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==2025.5.1"]
|
||||
"requirements": ["aioautomower==2025.6.0"]
|
||||
}
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==2.0.8"]
|
||||
"requirements": ["pylamarzocco==2.0.9"]
|
||||
}
|
||||
|
||||
@@ -58,6 +58,10 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
CoffeeBoiler, machine.dashboard.config[WidgetType.CM_COFFEE_BOILER]
|
||||
).target_temperature
|
||||
),
|
||||
available_fn=(
|
||||
lambda coordinator: WidgetType.CM_COFFEE_BOILER
|
||||
in coordinator.device.dashboard.config
|
||||
),
|
||||
),
|
||||
LaMarzoccoNumberEntityDescription(
|
||||
key="smart_standby_time",
|
||||
@@ -221,7 +225,7 @@ class LaMarzoccoNumberEntity(LaMarzoccoEntity, NumberEntity):
|
||||
entity_description: LaMarzoccoNumberEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
def native_value(self) -> float | int:
|
||||
"""Return the current value."""
|
||||
return self.entity_description.native_value_fn(self.coordinator.device)
|
||||
|
||||
|
||||
@@ -57,6 +57,10 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
|
||||
).ready_start_time
|
||||
),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
available_fn=(
|
||||
lambda coordinator: WidgetType.CM_COFFEE_BOILER
|
||||
in coordinator.device.dashboard.config
|
||||
),
|
||||
),
|
||||
LaMarzoccoSensorEntityDescription(
|
||||
key="steam_boiler_ready_time",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==10.0.4"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==10.0.4"]
|
||||
}
|
||||
|
||||
@@ -814,19 +814,6 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Flag media player features that are supported."""
|
||||
return self._attr_supported_features
|
||||
|
||||
@property
|
||||
def supported_features_compat(self) -> MediaPlayerEntityFeature:
|
||||
"""Return the supported features as MediaPlayerEntityFeature.
|
||||
|
||||
Remove this compatibility shim in 2025.1 or later.
|
||||
"""
|
||||
features = self.supported_features
|
||||
if type(features) is int:
|
||||
new_features = MediaPlayerEntityFeature(features)
|
||||
self._report_deprecated_supported_features_values(new_features)
|
||||
return new_features
|
||||
return features
|
||||
|
||||
def turn_on(self) -> None:
|
||||
"""Turn the media player on."""
|
||||
raise NotImplementedError
|
||||
@@ -966,87 +953,85 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
@property
|
||||
def support_play(self) -> bool:
|
||||
"""Boolean if play is supported."""
|
||||
return MediaPlayerEntityFeature.PLAY in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.PLAY in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_pause(self) -> bool:
|
||||
"""Boolean if pause is supported."""
|
||||
return MediaPlayerEntityFeature.PAUSE in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.PAUSE in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_stop(self) -> bool:
|
||||
"""Boolean if stop is supported."""
|
||||
return MediaPlayerEntityFeature.STOP in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.STOP in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_seek(self) -> bool:
|
||||
"""Boolean if seek is supported."""
|
||||
return MediaPlayerEntityFeature.SEEK in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.SEEK in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_volume_set(self) -> bool:
|
||||
"""Boolean if setting volume is supported."""
|
||||
return MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_volume_mute(self) -> bool:
|
||||
"""Boolean if muting volume is supported."""
|
||||
return MediaPlayerEntityFeature.VOLUME_MUTE in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.VOLUME_MUTE in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_previous_track(self) -> bool:
|
||||
"""Boolean if previous track command supported."""
|
||||
return MediaPlayerEntityFeature.PREVIOUS_TRACK in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.PREVIOUS_TRACK in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_next_track(self) -> bool:
|
||||
"""Boolean if next track command supported."""
|
||||
return MediaPlayerEntityFeature.NEXT_TRACK in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.NEXT_TRACK in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_play_media(self) -> bool:
|
||||
"""Boolean if play media command supported."""
|
||||
return MediaPlayerEntityFeature.PLAY_MEDIA in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.PLAY_MEDIA in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_select_source(self) -> bool:
|
||||
"""Boolean if select source command supported."""
|
||||
return MediaPlayerEntityFeature.SELECT_SOURCE in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.SELECT_SOURCE in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_select_sound_mode(self) -> bool:
|
||||
"""Boolean if select sound mode command supported."""
|
||||
return (
|
||||
MediaPlayerEntityFeature.SELECT_SOUND_MODE in self.supported_features_compat
|
||||
)
|
||||
return MediaPlayerEntityFeature.SELECT_SOUND_MODE in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_clear_playlist(self) -> bool:
|
||||
"""Boolean if clear playlist command supported."""
|
||||
return MediaPlayerEntityFeature.CLEAR_PLAYLIST in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.CLEAR_PLAYLIST in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_shuffle_set(self) -> bool:
|
||||
"""Boolean if shuffle is supported."""
|
||||
return MediaPlayerEntityFeature.SHUFFLE_SET in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.SHUFFLE_SET in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_grouping(self) -> bool:
|
||||
"""Boolean if player grouping is supported."""
|
||||
return MediaPlayerEntityFeature.GROUPING in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.GROUPING in self.supported_features
|
||||
|
||||
async def async_toggle(self) -> None:
|
||||
"""Toggle the power on the media player."""
|
||||
@@ -1074,7 +1059,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if (
|
||||
self.volume_level is not None
|
||||
and self.volume_level < 1
|
||||
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat
|
||||
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
|
||||
):
|
||||
await self.async_set_volume_level(
|
||||
min(1, self.volume_level + self.volume_step)
|
||||
@@ -1092,7 +1077,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if (
|
||||
self.volume_level is not None
|
||||
and self.volume_level > 0
|
||||
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat
|
||||
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
|
||||
):
|
||||
await self.async_set_volume_level(
|
||||
max(0, self.volume_level - self.volume_step)
|
||||
@@ -1135,7 +1120,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
def capability_attributes(self) -> dict[str, Any]:
|
||||
"""Return capability attributes."""
|
||||
data: dict[str, Any] = {}
|
||||
supported_features = self.supported_features_compat
|
||||
supported_features = self.supported_features
|
||||
|
||||
if (
|
||||
source_list := self.source_list
|
||||
@@ -1364,7 +1349,7 @@ async def websocket_browse_media(
|
||||
connection.send_error(msg["id"], "entity_not_found", "Entity not found")
|
||||
return
|
||||
|
||||
if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features_compat:
|
||||
if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features:
|
||||
connection.send_message(
|
||||
websocket_api.error_message(
|
||||
msg["id"], ERR_NOT_SUPPORTED, "Player does not support browsing media"
|
||||
@@ -1447,7 +1432,7 @@ async def websocket_search_media(
|
||||
connection.send_error(msg["id"], "entity_not_found", "Entity not found")
|
||||
return
|
||||
|
||||
if MediaPlayerEntityFeature.SEARCH_MEDIA not in player.supported_features_compat:
|
||||
if MediaPlayerEntityFeature.SEARCH_MEDIA not in player.supported_features:
|
||||
connection.send_message(
|
||||
websocket_api.error_message(
|
||||
msg["id"], ERR_NOT_SUPPORTED, "Player does not support searching media"
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import dns.asyncresolver
|
||||
import dns.rdata
|
||||
import dns.rdataclass
|
||||
import dns.rdatatype
|
||||
@@ -22,20 +23,23 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_dnspython_rdata_classes() -> None:
|
||||
"""Load dnspython rdata classes used by mcstatus."""
|
||||
def prevent_dnspython_blocking_operations() -> None:
|
||||
"""Prevent dnspython blocking operations by pre-loading required data."""
|
||||
|
||||
# Blocking import: https://github.com/rthalley/dnspython/issues/1083
|
||||
for rdtype in dns.rdatatype.RdataType:
|
||||
if not dns.rdatatype.is_metatype(rdtype) or rdtype == dns.rdatatype.OPT:
|
||||
dns.rdata.get_rdata_class(dns.rdataclass.IN, rdtype) # type: ignore[no-untyped-call]
|
||||
|
||||
# Blocking open: https://github.com/rthalley/dnspython/issues/1200
|
||||
dns.asyncresolver.get_default_resolver()
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: MinecraftServerConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Minecraft Server from a config entry."""
|
||||
|
||||
# Workaround to avoid blocking imports from dnspython (https://github.com/rthalley/dnspython/issues/1083)
|
||||
await hass.async_add_executor_job(load_dnspython_rdata_classes)
|
||||
await hass.async_add_executor_job(prevent_dnspython_blocking_operations)
|
||||
|
||||
# Create coordinator instance and store it.
|
||||
coordinator = MinecraftServerCoordinator(hass, entry)
|
||||
|
||||
@@ -35,7 +35,6 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
|
||||
from homeassistant.helpers.typing import ConfigType, VolSchemaType
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -48,7 +47,6 @@ from .const import (
|
||||
CONF_OPTIONS,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_SUGGESTED_DISPLAY_PRECISION,
|
||||
DOMAIN,
|
||||
PAYLOAD_NONE,
|
||||
)
|
||||
from .entity import MqttAvailabilityMixin, MqttEntity, async_setup_entity_entry_helper
|
||||
@@ -138,12 +136,9 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
|
||||
device_class in DEVICE_CLASS_UNITS
|
||||
and unit_of_measurement not in DEVICE_CLASS_UNITS[device_class]
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"The unit of measurement `%s` is not valid "
|
||||
"together with device class `%s`. "
|
||||
"this will stop working in HA Core 2025.7.0",
|
||||
unit_of_measurement,
|
||||
device_class,
|
||||
raise vol.Invalid(
|
||||
f"The unit of measurement `{unit_of_measurement}` is not valid "
|
||||
f"together with device class `{device_class}`",
|
||||
)
|
||||
|
||||
return config
|
||||
@@ -194,40 +189,8 @@ class MqttSensor(MqttEntity, RestoreSensor):
|
||||
None
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_check_uom(self) -> None:
|
||||
"""Check if the unit of measurement is valid with the device class."""
|
||||
if (
|
||||
self._discovery_data is not None
|
||||
or self.device_class is None
|
||||
or self.native_unit_of_measurement is None
|
||||
):
|
||||
return
|
||||
if (
|
||||
self.device_class in DEVICE_CLASS_UNITS
|
||||
and self.native_unit_of_measurement
|
||||
not in DEVICE_CLASS_UNITS[self.device_class]
|
||||
):
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
self.entity_id,
|
||||
issue_domain=sensor.DOMAIN,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
learn_more_url=URL_DOCS_SUPPORTED_SENSOR_UOM,
|
||||
translation_placeholders={
|
||||
"uom": self.native_unit_of_measurement,
|
||||
"device_class": self.device_class.value,
|
||||
"entity_id": self.entity_id,
|
||||
},
|
||||
translation_key="invalid_unit_of_measurement",
|
||||
breaks_in_ha_version="2025.7.0",
|
||||
)
|
||||
|
||||
async def mqtt_async_added_to_hass(self) -> None:
|
||||
"""Restore state for entities with expire_after set."""
|
||||
self.async_check_uom()
|
||||
last_state: State | None
|
||||
last_sensor_data: SensorExtraStoredData | None
|
||||
if (
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
"invalid_platform_config": {
|
||||
"title": "Invalid config found for MQTT {domain} item",
|
||||
"description": "Home Assistant detected an invalid config for a manually configured item.\n\nPlatform domain: **{domain}**\nConfiguration file: **{config_file}**\nNear line: **{line}**\nConfiguration found:\n```yaml\n{config}\n```\nError: **{error}**.\n\nMake sure the configuration is valid and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue."
|
||||
},
|
||||
"invalid_unit_of_measurement": {
|
||||
"title": "Sensor with invalid unit of measurement",
|
||||
"description": "Manual configured Sensor entity **{entity_id}** has a configured unit of measurement **{uom}** which is not valid with configured device class **{device_class}**. Make sure a valid unit of measurement is configured or remove the device class, and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue."
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "API Key for your NextDNS account"
|
||||
"api_key": "The API key for your NextDNS account"
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"data": {
|
||||
"profile": "Profile"
|
||||
"profile_name": "Profile"
|
||||
},
|
||||
"data_description": {
|
||||
"profile": "NextDNS configuration profile you want to integrate"
|
||||
"profile_name": "The NextDNS configuration profile you want to integrate"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
|
||||
@@ -66,7 +66,7 @@ class OneDriveUpdateCoordinator(DataUpdateCoordinator[Drive]):
|
||||
translation_domain=DOMAIN, translation_key="authentication_failed"
|
||||
) from err
|
||||
except OneDriveException as err:
|
||||
_LOGGER.debug("Failed to fetch drive data: %s")
|
||||
_LOGGER.debug("Failed to fetch drive data: %s", err, exc_info=True)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN, translation_key="update_failed"
|
||||
) from err
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ovoenergy"],
|
||||
"requirements": ["ovoenergy==2.0.0"]
|
||||
"requirements": ["ovoenergy==2.0.1"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ical"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==10.0.4"]
|
||||
}
|
||||
|
||||
@@ -75,7 +75,10 @@ class ReolinkHostCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinator[None]
|
||||
)
|
||||
|
||||
http_s = "https" if self._host.api.use_https else "http"
|
||||
self._conf_url = f"{http_s}://{self._host.api.host}:{self._host.api.port}"
|
||||
if self._host.api.baichuan_only:
|
||||
self._conf_url = None
|
||||
else:
|
||||
self._conf_url = f"{http_s}://{self._host.api.host}:{self._host.api.port}"
|
||||
self._dev_id = self._host.unique_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._dev_id)},
|
||||
@@ -184,6 +187,11 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity):
|
||||
if mac := self._host.api.baichuan.mac_address(dev_ch):
|
||||
connections.add((CONNECTION_NETWORK_MAC, mac))
|
||||
|
||||
if self._conf_url is None:
|
||||
conf_url = None
|
||||
else:
|
||||
conf_url = f"{self._conf_url}/?ch={dev_ch}"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._dev_id)},
|
||||
connections=connections,
|
||||
@@ -195,7 +203,7 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity):
|
||||
hw_version=self._host.api.camera_hardware_version(dev_ch),
|
||||
sw_version=self._host.api.camera_sw_version(dev_ch),
|
||||
serial_number=self._host.api.camera_uid(dev_ch),
|
||||
configuration_url=f"{self._conf_url}/?ch={dev_ch}",
|
||||
configuration_url=conf_url,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiorussound"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiorussound==4.5.2"],
|
||||
"requirements": ["aiorussound==4.6.0"],
|
||||
"zeroconf": ["_rio._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -22,12 +22,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: FederwiegeConfigEntry) -
|
||||
|
||||
federwiege = Federwiege(hass.loop, connection)
|
||||
federwiege.register()
|
||||
federwiege.connect()
|
||||
|
||||
entry.runtime_data = federwiege
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
federwiege.connect()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmarlaapi", "pysignalr"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmarlaapi==0.8.2"]
|
||||
"requirements": ["pysmarlaapi==0.9.0"]
|
||||
}
|
||||
|
||||
@@ -53,9 +53,10 @@ class SmarlaNumber(SmarlaBaseEntity, NumberEntity):
|
||||
_property: Property[int]
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the entity value to represent the entity state."""
|
||||
return self._property.get()
|
||||
v = self._property.get()
|
||||
return float(v) if v is not None else None
|
||||
|
||||
def set_native_value(self, value: float) -> None:
|
||||
"""Update to the smarla device."""
|
||||
|
||||
@@ -52,7 +52,7 @@ class SmarlaSwitch(SmarlaBaseEntity, SwitchEntity):
|
||||
_property: Property[bool]
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the entity value to represent the entity state."""
|
||||
return self._property.get()
|
||||
|
||||
|
||||
@@ -30,5 +30,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmartthings==3.2.4"]
|
||||
"requirements": ["pysmartthings==3.2.5"]
|
||||
}
|
||||
|
||||
@@ -605,10 +605,10 @@
|
||||
"name": "Wrinkle prevent"
|
||||
},
|
||||
"ice_maker": {
|
||||
"name": "Ice cubes"
|
||||
"name": "Cubed ice"
|
||||
},
|
||||
"ice_maker_2": {
|
||||
"name": "Ice bites"
|
||||
"name": "Ice Bites"
|
||||
},
|
||||
"sabbath_mode": {
|
||||
"name": "Sabbath mode"
|
||||
|
||||
@@ -41,13 +41,12 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.script import Script
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN
|
||||
from .const import CONF_OBJECT_ID, DOMAIN
|
||||
from .entity import AbstractTemplateEntity
|
||||
from .template_entity import (
|
||||
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
|
||||
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
|
||||
TEMPLATE_ENTITY_ICON_SCHEMA,
|
||||
TemplateEntity,
|
||||
make_template_entity_common_modern_schema,
|
||||
rewrite_common_legacy_to_modern_conf,
|
||||
)
|
||||
|
||||
@@ -105,15 +104,10 @@ ALARM_CONTROL_PANEL_SCHEMA = vol.All(
|
||||
CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name
|
||||
): cv.enum(TemplateCodeFormat),
|
||||
vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_NAME): cv.template,
|
||||
vol.Optional(CONF_PICTURE): cv.template,
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
|
||||
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema),
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
)
|
||||
|
||||
|
||||
@@ -419,9 +413,7 @@ class AlarmControlPanelTemplate(TemplateEntity, AbstractTemplateAlarmControlPane
|
||||
unique_id: str | None,
|
||||
) -> None:
|
||||
"""Initialize the panel."""
|
||||
TemplateEntity.__init__(
|
||||
self, hass, config=config, fallback_name=None, unique_id=unique_id
|
||||
)
|
||||
TemplateEntity.__init__(self, hass, config=config, unique_id=unique_id)
|
||||
AbstractTemplateAlarmControlPanel.__init__(self, config)
|
||||
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
|
||||
self.entity_id = async_generate_entity_id(
|
||||
|
||||
@@ -26,29 +26,19 @@ from homeassistant.helpers.entity_platform import (
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import CONF_PRESS, DOMAIN
|
||||
from .template_entity import (
|
||||
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
|
||||
TEMPLATE_ENTITY_ICON_SCHEMA,
|
||||
TemplateEntity,
|
||||
)
|
||||
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "Template Button"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
BUTTON_SCHEMA = (
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template,
|
||||
vol.Required(CONF_PRESS): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
|
||||
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema)
|
||||
)
|
||||
BUTTON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PRESS): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
}
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
|
||||
CONFIG_BUTTON_SCHEMA = vol.Schema(
|
||||
{
|
||||
|
||||
@@ -37,14 +37,13 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN
|
||||
from .const import CONF_OBJECT_ID, DOMAIN
|
||||
from .entity import AbstractTemplateEntity
|
||||
from .template_entity import (
|
||||
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
|
||||
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
|
||||
TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY,
|
||||
TEMPLATE_ENTITY_ICON_SCHEMA,
|
||||
TemplateEntity,
|
||||
make_template_entity_common_modern_schema,
|
||||
rewrite_common_legacy_to_modern_conf,
|
||||
)
|
||||
from .trigger_entity import TriggerEntity
|
||||
@@ -100,21 +99,16 @@ COVER_SCHEMA = vol.All(
|
||||
vol.Inclusive(CLOSE_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA,
|
||||
vol.Inclusive(OPEN_ACTION, CONF_OPEN_AND_CLOSE): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template,
|
||||
vol.Optional(CONF_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_PICTURE): cv.template,
|
||||
vol.Optional(CONF_POSITION): cv.template,
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_TILT): cv.template,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(STOP_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA,
|
||||
}
|
||||
)
|
||||
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
|
||||
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema),
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema),
|
||||
cv.has_at_least_one_key(OPEN_ACTION, POSITION_ACTION),
|
||||
)
|
||||
|
||||
@@ -463,9 +457,7 @@ class CoverTemplate(TemplateEntity, AbstractTemplateCover):
|
||||
unique_id,
|
||||
) -> None:
|
||||
"""Initialize the Template cover."""
|
||||
TemplateEntity.__init__(
|
||||
self, hass, config=config, fallback_name=None, unique_id=unique_id
|
||||
)
|
||||
TemplateEntity.__init__(self, hass, config=config, unique_id=unique_id)
|
||||
AbstractTemplateCover.__init__(self, config)
|
||||
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
|
||||
self.entity_id = async_generate_entity_id(
|
||||
|
||||
@@ -37,14 +37,13 @@ from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN
|
||||
from .const import CONF_OBJECT_ID, DOMAIN
|
||||
from .entity import AbstractTemplateEntity
|
||||
from .template_entity import (
|
||||
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
|
||||
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
|
||||
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY,
|
||||
TEMPLATE_ENTITY_ICON_SCHEMA,
|
||||
TemplateEntity,
|
||||
make_template_entity_common_modern_schema,
|
||||
rewrite_common_legacy_to_modern_conf,
|
||||
)
|
||||
|
||||
@@ -85,12 +84,10 @@ FAN_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_DIRECTION): cv.template,
|
||||
vol.Optional(CONF_NAME): cv.template,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_OSCILLATING): cv.template,
|
||||
vol.Optional(CONF_PERCENTAGE): cv.template,
|
||||
vol.Optional(CONF_PICTURE): cv.template,
|
||||
vol.Optional(CONF_PRESET_MODE): cv.template,
|
||||
vol.Optional(CONF_PRESET_MODES): cv.ensure_list,
|
||||
vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA,
|
||||
@@ -99,11 +96,8 @@ FAN_SCHEMA = vol.All(
|
||||
vol.Optional(CONF_SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_SPEED_COUNT): vol.Coerce(int),
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
|
||||
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema),
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
)
|
||||
|
||||
LEGACY_FAN_SCHEMA = vol.All(
|
||||
@@ -488,9 +482,7 @@ class TemplateFan(TemplateEntity, AbstractTemplateFan):
|
||||
unique_id,
|
||||
) -> None:
|
||||
"""Initialize the fan."""
|
||||
TemplateEntity.__init__(
|
||||
self, hass, config=config, fallback_name=None, unique_id=unique_id
|
||||
)
|
||||
TemplateEntity.__init__(self, hass, config=config, unique_id=unique_id)
|
||||
AbstractTemplateFan.__init__(self, config)
|
||||
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
|
||||
self.entity_id = async_generate_entity_id(
|
||||
|
||||
@@ -29,7 +29,10 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import CONF_PICTURE
|
||||
from .template_entity import TemplateEntity, make_template_entity_common_schema
|
||||
from .template_entity import (
|
||||
TemplateEntity,
|
||||
make_template_entity_common_modern_attributes_schema,
|
||||
)
|
||||
from .trigger_entity import TriggerEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -43,7 +46,7 @@ IMAGE_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_URL): cv.template,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): bool,
|
||||
}
|
||||
).extend(make_template_entity_common_schema(DEFAULT_NAME).schema)
|
||||
).extend(make_template_entity_common_modern_attributes_schema(DEFAULT_NAME).schema)
|
||||
|
||||
|
||||
IMAGE_CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
@@ -49,14 +49,13 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN
|
||||
from .const import CONF_OBJECT_ID, DOMAIN
|
||||
from .entity import AbstractTemplateEntity
|
||||
from .template_entity import (
|
||||
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
|
||||
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
|
||||
TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY,
|
||||
TEMPLATE_ENTITY_ICON_SCHEMA,
|
||||
TemplateEntity,
|
||||
make_template_entity_common_modern_schema,
|
||||
rewrite_common_legacy_to_modern_conf,
|
||||
)
|
||||
from .trigger_entity import TriggerEntity
|
||||
@@ -124,38 +123,31 @@ LEGACY_FIELDS = TEMPLATE_ENTITY_LEGACY_FIELDS | {
|
||||
|
||||
DEFAULT_NAME = "Template Light"
|
||||
|
||||
LIGHT_SCHEMA = (
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Inclusive(CONF_EFFECT_ACTION, "effect"): cv.SCRIPT_SCHEMA,
|
||||
vol.Inclusive(CONF_EFFECT_LIST, "effect"): cv.template,
|
||||
vol.Inclusive(CONF_EFFECT, "effect"): cv.template,
|
||||
vol.Optional(CONF_HS_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_HS): cv.template,
|
||||
vol.Optional(CONF_LEVEL_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_LEVEL): cv.template,
|
||||
vol.Optional(CONF_MAX_MIREDS): cv.template,
|
||||
vol.Optional(CONF_MIN_MIREDS): cv.template,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template,
|
||||
vol.Optional(CONF_PICTURE): cv.template,
|
||||
vol.Optional(CONF_RGB_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_RGB): cv.template,
|
||||
vol.Optional(CONF_RGBW_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_RGBW): cv.template,
|
||||
vol.Optional(CONF_RGBWW_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_RGBWW): cv.template,
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_SUPPORTS_TRANSITION): cv.template,
|
||||
vol.Optional(CONF_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_TEMPERATURE): cv.template,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
}
|
||||
)
|
||||
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
|
||||
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema)
|
||||
)
|
||||
LIGHT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Inclusive(CONF_EFFECT_ACTION, "effect"): cv.SCRIPT_SCHEMA,
|
||||
vol.Inclusive(CONF_EFFECT_LIST, "effect"): cv.template,
|
||||
vol.Inclusive(CONF_EFFECT, "effect"): cv.template,
|
||||
vol.Optional(CONF_HS_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_HS): cv.template,
|
||||
vol.Optional(CONF_LEVEL_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_LEVEL): cv.template,
|
||||
vol.Optional(CONF_MAX_MIREDS): cv.template,
|
||||
vol.Optional(CONF_MIN_MIREDS): cv.template,
|
||||
vol.Optional(CONF_RGB_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_RGB): cv.template,
|
||||
vol.Optional(CONF_RGBW_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_RGBW): cv.template,
|
||||
vol.Optional(CONF_RGBWW_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_RGBWW): cv.template,
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_SUPPORTS_TRANSITION): cv.template,
|
||||
vol.Optional(CONF_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_TEMPERATURE): cv.template,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
}
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
|
||||
LEGACY_LIGHT_SCHEMA = vol.All(
|
||||
cv.deprecated(CONF_ENTITY_ID),
|
||||
@@ -955,9 +947,7 @@ class LightTemplate(TemplateEntity, AbstractTemplateLight):
|
||||
unique_id: str | None,
|
||||
) -> None:
|
||||
"""Initialize the light."""
|
||||
TemplateEntity.__init__(
|
||||
self, hass, config=config, fallback_name=None, unique_id=unique_id
|
||||
)
|
||||
TemplateEntity.__init__(self, hass, config=config, unique_id=unique_id)
|
||||
AbstractTemplateLight.__init__(self, config)
|
||||
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
|
||||
self.entity_id = async_generate_entity_id(
|
||||
|
||||
@@ -31,10 +31,9 @@ from .const import CONF_PICTURE, DOMAIN
|
||||
from .entity import AbstractTemplateEntity
|
||||
from .template_entity import (
|
||||
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
|
||||
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
|
||||
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY,
|
||||
TEMPLATE_ENTITY_ICON_SCHEMA,
|
||||
TemplateEntity,
|
||||
make_template_entity_common_modern_schema,
|
||||
rewrite_common_legacy_to_modern_conf,
|
||||
)
|
||||
|
||||
@@ -57,17 +56,13 @@ LOCK_SCHEMA = vol.All(
|
||||
{
|
||||
vol.Optional(CONF_CODE_FORMAT): cv.template,
|
||||
vol.Required(CONF_LOCK): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_NAME): cv.template,
|
||||
vol.Optional(CONF_OPEN): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_PICTURE): cv.template,
|
||||
vol.Required(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Required(CONF_UNLOCK): cv.SCRIPT_SCHEMA,
|
||||
}
|
||||
)
|
||||
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
|
||||
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema),
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
)
|
||||
|
||||
|
||||
@@ -313,9 +308,7 @@ class TemplateLock(TemplateEntity, AbstractTemplateLock):
|
||||
unique_id: str | None,
|
||||
) -> None:
|
||||
"""Initialize the lock."""
|
||||
TemplateEntity.__init__(
|
||||
self, hass, config=config, fallback_name=DEFAULT_NAME, unique_id=unique_id
|
||||
)
|
||||
TemplateEntity.__init__(self, hass, config=config, unique_id=unique_id)
|
||||
AbstractTemplateLock.__init__(self, config)
|
||||
name = self._attr_name
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -35,11 +35,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import CONF_MAX, CONF_MIN, CONF_STEP, DOMAIN
|
||||
from .template_entity import (
|
||||
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
|
||||
TEMPLATE_ENTITY_ICON_SCHEMA,
|
||||
TemplateEntity,
|
||||
)
|
||||
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema
|
||||
from .trigger_entity import TriggerEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -49,23 +45,17 @@ CONF_SET_VALUE = "set_value"
|
||||
DEFAULT_NAME = "Template Number"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
NUMBER_SCHEMA = (
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template,
|
||||
vol.Required(CONF_STATE): cv.template,
|
||||
vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_STEP): cv.template,
|
||||
vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): cv.template,
|
||||
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
|
||||
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema)
|
||||
)
|
||||
NUMBER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_STATE): cv.template,
|
||||
vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_STEP): cv.template,
|
||||
vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): cv.template,
|
||||
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
}
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
NUMBER_CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.template,
|
||||
|
||||
@@ -32,11 +32,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .template_entity import (
|
||||
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
|
||||
TEMPLATE_ENTITY_ICON_SCHEMA,
|
||||
TemplateEntity,
|
||||
)
|
||||
from .template_entity import TemplateEntity, make_template_entity_common_modern_schema
|
||||
from .trigger_entity import TriggerEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -47,20 +43,14 @@ CONF_SELECT_OPTION = "select_option"
|
||||
DEFAULT_NAME = "Template Select"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
SELECT_SCHEMA = (
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template,
|
||||
vol.Required(CONF_STATE): cv.template,
|
||||
vol.Required(CONF_SELECT_OPTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(ATTR_OPTIONS): cv.template,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
|
||||
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema)
|
||||
)
|
||||
SELECT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_STATE): cv.template,
|
||||
vol.Required(CONF_SELECT_OPTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(ATTR_OPTIONS): cv.template,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
}
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
|
||||
|
||||
SELECT_CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
@@ -40,13 +40,12 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import CONF_OBJECT_ID, CONF_PICTURE, CONF_TURN_OFF, CONF_TURN_ON, DOMAIN
|
||||
from .const import CONF_OBJECT_ID, CONF_TURN_OFF, CONF_TURN_ON, DOMAIN
|
||||
from .template_entity import (
|
||||
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
|
||||
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
|
||||
TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY,
|
||||
TEMPLATE_ENTITY_ICON_SCHEMA,
|
||||
TemplateEntity,
|
||||
make_template_entity_common_modern_schema,
|
||||
rewrite_common_legacy_to_modern_conf,
|
||||
)
|
||||
from .trigger_entity import TriggerEntity
|
||||
@@ -60,20 +59,13 @@ LEGACY_FIELDS = TEMPLATE_ENTITY_LEGACY_FIELDS | {
|
||||
DEFAULT_NAME = "Template Switch"
|
||||
|
||||
|
||||
SWITCH_SCHEMA = (
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template,
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Required(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_PICTURE): cv.template,
|
||||
}
|
||||
)
|
||||
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
|
||||
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema)
|
||||
)
|
||||
SWITCH_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Required(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
|
||||
}
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
|
||||
LEGACY_SWITCH_SCHEMA = vol.All(
|
||||
cv.deprecated(ATTR_ENTITY_ID),
|
||||
@@ -228,7 +220,7 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity):
|
||||
unique_id: str | None,
|
||||
) -> None:
|
||||
"""Initialize the Template switch."""
|
||||
super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id)
|
||||
super().__init__(hass, config=config, unique_id=unique_id)
|
||||
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
|
||||
self.entity_id = async_generate_entity_id(
|
||||
ENTITY_ID_FORMAT, object_id, hass=hass
|
||||
|
||||
@@ -94,16 +94,24 @@ TEMPLATE_ENTITY_COMMON_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
def make_template_entity_common_schema(default_name: str) -> vol.Schema:
|
||||
def make_template_entity_common_modern_schema(
|
||||
default_name: str,
|
||||
) -> vol.Schema:
|
||||
"""Return a schema with default name."""
|
||||
return (
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_AVAILABILITY): cv.template,
|
||||
}
|
||||
)
|
||||
.extend(make_template_entity_base_schema(default_name).schema)
|
||||
.extend(TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA.schema)
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_AVAILABILITY): cv.template,
|
||||
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
|
||||
}
|
||||
).extend(make_template_entity_base_schema(default_name).schema)
|
||||
|
||||
|
||||
def make_template_entity_common_modern_attributes_schema(
|
||||
default_name: str,
|
||||
) -> vol.Schema:
|
||||
"""Return a schema with default name."""
|
||||
return make_template_entity_common_modern_schema(default_name).extend(
|
||||
TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA.schema
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -38,16 +38,14 @@ from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN
|
||||
from .const import CONF_OBJECT_ID, DOMAIN
|
||||
from .entity import AbstractTemplateEntity
|
||||
from .template_entity import (
|
||||
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
|
||||
TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA,
|
||||
TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA_LEGACY,
|
||||
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
|
||||
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY,
|
||||
TEMPLATE_ENTITY_ICON_SCHEMA,
|
||||
TemplateEntity,
|
||||
make_template_entity_common_modern_attributes_schema,
|
||||
rewrite_common_legacy_to_modern_conf,
|
||||
)
|
||||
|
||||
@@ -60,6 +58,8 @@ CONF_FAN_SPEED_LIST = "fan_speeds"
|
||||
CONF_FAN_SPEED = "fan_speed"
|
||||
CONF_FAN_SPEED_TEMPLATE = "fan_speed_template"
|
||||
|
||||
DEFAULT_NAME = "Template Vacuum"
|
||||
|
||||
ENTITY_ID_FORMAT = VACUUM_DOMAIN + ".{}"
|
||||
_VALID_STATES = [
|
||||
VacuumActivity.CLEANING,
|
||||
@@ -80,13 +80,9 @@ VACUUM_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_BATTERY_LEVEL): cv.template,
|
||||
vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(CONF_FAN_SPEED_LIST, default=[]): cv.ensure_list,
|
||||
vol.Optional(CONF_FAN_SPEED): cv.template,
|
||||
vol.Optional(CONF_NAME): cv.template,
|
||||
vol.Optional(CONF_PICTURE): cv.template,
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(SERVICE_CLEAN_SPOT): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(SERVICE_LOCATE): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(SERVICE_PAUSE): cv.SCRIPT_SCHEMA,
|
||||
@@ -95,10 +91,7 @@ VACUUM_SCHEMA = vol.All(
|
||||
vol.Required(SERVICE_START): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(SERVICE_STOP): cv.SCRIPT_SCHEMA,
|
||||
}
|
||||
)
|
||||
.extend(TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA.schema)
|
||||
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
|
||||
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema),
|
||||
).extend(make_template_entity_common_modern_attributes_schema(DEFAULT_NAME).schema)
|
||||
)
|
||||
|
||||
LEGACY_VACUUM_SCHEMA = vol.All(
|
||||
@@ -353,9 +346,7 @@ class TemplateVacuum(TemplateEntity, AbstractTemplateVacuum):
|
||||
unique_id,
|
||||
) -> None:
|
||||
"""Initialize the vacuum."""
|
||||
TemplateEntity.__init__(
|
||||
self, hass, config=config, fallback_name=None, unique_id=unique_id
|
||||
)
|
||||
TemplateEntity.__init__(self, hass, config=config, unique_id=unique_id)
|
||||
AbstractTemplateVacuum.__init__(self, config)
|
||||
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
|
||||
self.entity_id = async_generate_entity_id(
|
||||
|
||||
@@ -32,7 +32,6 @@ from homeassistant.components.weather import (
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_TEMPERATURE_UNIT,
|
||||
CONF_UNIQUE_ID,
|
||||
STATE_UNAVAILABLE,
|
||||
@@ -53,7 +52,11 @@ from homeassistant.util.unit_conversion import (
|
||||
)
|
||||
|
||||
from .coordinator import TriggerUpdateCoordinator
|
||||
from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf
|
||||
from .template_entity import (
|
||||
TemplateEntity,
|
||||
make_template_entity_common_modern_schema,
|
||||
rewrite_common_legacy_to_modern_conf,
|
||||
)
|
||||
from .trigger_entity import TriggerEntity
|
||||
|
||||
CHECK_FORECAST_KEYS = (
|
||||
@@ -104,33 +107,33 @@ CONF_CLOUD_COVERAGE_TEMPLATE = "cloud_coverage_template"
|
||||
CONF_DEW_POINT_TEMPLATE = "dew_point_template"
|
||||
CONF_APPARENT_TEMPERATURE_TEMPLATE = "apparent_temperature_template"
|
||||
|
||||
DEFAULT_NAME = "Template Weather"
|
||||
|
||||
WEATHER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.template,
|
||||
vol.Required(CONF_CONDITION_TEMPLATE): cv.template,
|
||||
vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template,
|
||||
vol.Required(CONF_HUMIDITY_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_APPARENT_TEMPERATURE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_ATTRIBUTION_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_PRESSURE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_WIND_SPEED_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_OZONE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_CLOUD_COVERAGE_TEMPLATE): cv.template,
|
||||
vol.Required(CONF_CONDITION_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_DEW_POINT_TEMPLATE): cv.template,
|
||||
vol.Required(CONF_HUMIDITY_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_FORECAST_DAILY_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_FORECAST_HOURLY_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_FORECAST_TWICE_DAILY_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(TemperatureConverter.VALID_UNITS),
|
||||
vol.Optional(CONF_PRESSURE_UNIT): vol.In(PressureConverter.VALID_UNITS),
|
||||
vol.Optional(CONF_WIND_SPEED_UNIT): vol.In(SpeedConverter.VALID_UNITS),
|
||||
vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS),
|
||||
vol.Optional(CONF_OZONE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_PRECIPITATION_UNIT): vol.In(DistanceConverter.VALID_UNITS),
|
||||
vol.Optional(CONF_PRESSURE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_PRESSURE_UNIT): vol.In(PressureConverter.VALID_UNITS),
|
||||
vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(TemperatureConverter.VALID_UNITS),
|
||||
vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS),
|
||||
vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_WIND_GUST_SPEED_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_CLOUD_COVERAGE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_DEW_POINT_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_APPARENT_TEMPERATURE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_WIND_SPEED_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_WIND_SPEED_UNIT): vol.In(SpeedConverter.VALID_UNITS),
|
||||
}
|
||||
)
|
||||
).extend(make_template_entity_common_modern_schema(DEFAULT_NAME).schema)
|
||||
|
||||
PLATFORM_SCHEMA = WEATHER_PLATFORM_SCHEMA.extend(WEATHER_SCHEMA.schema)
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect", "unifi_discovery"],
|
||||
"requirements": ["uiprotect==7.13.0", "unifi-discovery==1.2.0"],
|
||||
"requirements": ["uiprotect==7.14.0", "unifi-discovery==1.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -312,7 +312,7 @@ class StateVacuumEntity(
|
||||
@property
|
||||
def capability_attributes(self) -> dict[str, Any] | None:
|
||||
"""Return capability attributes."""
|
||||
if VacuumEntityFeature.FAN_SPEED in self.supported_features_compat:
|
||||
if VacuumEntityFeature.FAN_SPEED in self.supported_features:
|
||||
return {ATTR_FAN_SPEED_LIST: self.fan_speed_list}
|
||||
return None
|
||||
|
||||
@@ -330,7 +330,7 @@ class StateVacuumEntity(
|
||||
def state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes of the vacuum cleaner."""
|
||||
data: dict[str, Any] = {}
|
||||
supported_features = self.supported_features_compat
|
||||
supported_features = self.supported_features
|
||||
|
||||
if VacuumEntityFeature.BATTERY in supported_features:
|
||||
data[ATTR_BATTERY_LEVEL] = self.battery_level
|
||||
@@ -369,19 +369,6 @@ class StateVacuumEntity(
|
||||
"""Flag vacuum cleaner features that are supported."""
|
||||
return self._attr_supported_features
|
||||
|
||||
@property
|
||||
def supported_features_compat(self) -> VacuumEntityFeature:
|
||||
"""Return the supported features as VacuumEntityFeature.
|
||||
|
||||
Remove this compatibility shim in 2025.1 or later.
|
||||
"""
|
||||
features = self.supported_features
|
||||
if type(features) is int:
|
||||
new_features = VacuumEntityFeature(features)
|
||||
self._report_deprecated_supported_features_values(new_features)
|
||||
return new_features
|
||||
return features
|
||||
|
||||
def stop(self, **kwargs: Any) -> None:
|
||||
"""Stop the vacuum cleaner."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiowebdav2"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aiowebdav2==0.4.5"]
|
||||
"requirements": ["aiowebdav2==0.4.6"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["holidays"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["holidays==0.74"]
|
||||
"requirements": ["holidays==0.75"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
import io
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Final
|
||||
import wave
|
||||
|
||||
@@ -36,6 +37,7 @@ from homeassistant.components.assist_satellite import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.ulid import ulid_now
|
||||
|
||||
from .const import DOMAIN, SAMPLE_CHANNELS, SAMPLE_WIDTH
|
||||
from .data import WyomingService
|
||||
@@ -53,6 +55,7 @@ _PING_SEND_DELAY: Final = 2
|
||||
_PIPELINE_FINISH_TIMEOUT: Final = 1
|
||||
_TTS_SAMPLE_RATE: Final = 22050
|
||||
_ANNOUNCE_CHUNK_BYTES: Final = 2048 # 1024 samples
|
||||
_TTS_TIMEOUT_EXTRA: Final = 1.0
|
||||
|
||||
# Wyoming stage -> Assist stage
|
||||
_STAGES: dict[PipelineStage, assist_pipeline.PipelineStage] = {
|
||||
@@ -125,6 +128,10 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
|
||||
self._ffmpeg_manager: ffmpeg.FFmpegManager | None = None
|
||||
self._played_event_received: asyncio.Event | None = None
|
||||
|
||||
# Randomly set on each pipeline loop run.
|
||||
# Used to ensure TTS timeout is acted on correctly.
|
||||
self._run_loop_id: str | None = None
|
||||
|
||||
@property
|
||||
def pipeline_entity_id(self) -> str | None:
|
||||
"""Return the entity ID of the pipeline to use for the next conversation."""
|
||||
@@ -511,6 +518,7 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
|
||||
wake_word_phrase: str | None = None
|
||||
run_pipeline: RunPipeline | None = None
|
||||
send_ping = True
|
||||
self._run_loop_id = ulid_now()
|
||||
|
||||
# Read events and check for pipeline end in parallel
|
||||
pipeline_ended_task = self.config_entry.async_create_background_task(
|
||||
@@ -698,38 +706,52 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
|
||||
f"Cannot stream audio format to satellite: {tts_result.extension}"
|
||||
)
|
||||
|
||||
data = b"".join([chunk async for chunk in tts_result.async_stream_result()])
|
||||
# Track the total duration of TTS audio for response timeout
|
||||
total_seconds = 0.0
|
||||
start_time = time.monotonic()
|
||||
|
||||
with io.BytesIO(data) as wav_io, wave.open(wav_io, "rb") as wav_file:
|
||||
sample_rate = wav_file.getframerate()
|
||||
sample_width = wav_file.getsampwidth()
|
||||
sample_channels = wav_file.getnchannels()
|
||||
_LOGGER.debug("Streaming %s TTS sample(s)", wav_file.getnframes())
|
||||
try:
|
||||
data = b"".join([chunk async for chunk in tts_result.async_stream_result()])
|
||||
|
||||
timestamp = 0
|
||||
await self._client.write_event(
|
||||
AudioStart(
|
||||
rate=sample_rate,
|
||||
width=sample_width,
|
||||
channels=sample_channels,
|
||||
timestamp=timestamp,
|
||||
).event()
|
||||
)
|
||||
with io.BytesIO(data) as wav_io, wave.open(wav_io, "rb") as wav_file:
|
||||
sample_rate = wav_file.getframerate()
|
||||
sample_width = wav_file.getsampwidth()
|
||||
sample_channels = wav_file.getnchannels()
|
||||
_LOGGER.debug("Streaming %s TTS sample(s)", wav_file.getnframes())
|
||||
|
||||
# Stream audio chunks
|
||||
while audio_bytes := wav_file.readframes(_SAMPLES_PER_CHUNK):
|
||||
chunk = AudioChunk(
|
||||
rate=sample_rate,
|
||||
width=sample_width,
|
||||
channels=sample_channels,
|
||||
audio=audio_bytes,
|
||||
timestamp=timestamp,
|
||||
timestamp = 0
|
||||
await self._client.write_event(
|
||||
AudioStart(
|
||||
rate=sample_rate,
|
||||
width=sample_width,
|
||||
channels=sample_channels,
|
||||
timestamp=timestamp,
|
||||
).event()
|
||||
)
|
||||
await self._client.write_event(chunk.event())
|
||||
timestamp += chunk.seconds
|
||||
|
||||
await self._client.write_event(AudioStop(timestamp=timestamp).event())
|
||||
_LOGGER.debug("TTS streaming complete")
|
||||
# Stream audio chunks
|
||||
while audio_bytes := wav_file.readframes(_SAMPLES_PER_CHUNK):
|
||||
chunk = AudioChunk(
|
||||
rate=sample_rate,
|
||||
width=sample_width,
|
||||
channels=sample_channels,
|
||||
audio=audio_bytes,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
await self._client.write_event(chunk.event())
|
||||
timestamp += chunk.seconds
|
||||
total_seconds += chunk.seconds
|
||||
|
||||
await self._client.write_event(AudioStop(timestamp=timestamp).event())
|
||||
_LOGGER.debug("TTS streaming complete")
|
||||
finally:
|
||||
send_duration = time.monotonic() - start_time
|
||||
timeout_seconds = max(0, total_seconds - send_duration + _TTS_TIMEOUT_EXTRA)
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._tts_timeout(timeout_seconds, self._run_loop_id),
|
||||
name="wyoming TTS timeout",
|
||||
)
|
||||
|
||||
async def _stt_stream(self) -> AsyncGenerator[bytes]:
|
||||
"""Yield audio chunks from a queue."""
|
||||
@@ -744,6 +766,18 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
|
||||
|
||||
yield chunk
|
||||
|
||||
async def _tts_timeout(
|
||||
self, timeout_seconds: float, run_loop_id: str | None
|
||||
) -> None:
|
||||
"""Force state change to IDLE in case TTS played event isn't received."""
|
||||
await asyncio.sleep(timeout_seconds + _TTS_TIMEOUT_EXTRA)
|
||||
|
||||
if run_loop_id != self._run_loop_id:
|
||||
# On a different pipeline run now
|
||||
return
|
||||
|
||||
self.tts_response_finished()
|
||||
|
||||
@callback
|
||||
def _handle_timer(
|
||||
self, event_type: intent.TimerEventType, timer: intent.TimerInfo
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "xiaomi_miio",
|
||||
"name": "Xiaomi Miio",
|
||||
"name": "Xiaomi Home",
|
||||
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
|
||||
|
||||
@@ -5,37 +5,37 @@
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"incomplete_info": "Incomplete information to set up device, no host or token supplied.",
|
||||
"not_xiaomi_miio": "Device is not (yet) supported by Xiaomi Miio.",
|
||||
"not_xiaomi_miio": "Device is not (yet) supported by Xiaomi Home integration.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"wrong_token": "Checksum error, wrong token",
|
||||
"unknown_device": "The device model is not known, not able to set up the device using config flow.",
|
||||
"cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.",
|
||||
"cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country",
|
||||
"cloud_login_error": "Could not log in to Xiaomi Miio Cloud, check the credentials."
|
||||
"cloud_no_devices": "No devices found in this Xiaomi Home account.",
|
||||
"cloud_credentials_incomplete": "Credentials incomplete, please fill in username, password and server region",
|
||||
"cloud_login_error": "Could not log in to Xiaomi Home, check the credentials."
|
||||
},
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"description": "The Xiaomi Miio integration needs to re-authenticate your account in order to update the tokens or add missing cloud credentials.",
|
||||
"description": "The Xiaomi Home integration needs to re-authenticate your account in order to update the tokens or add missing credentials.",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
},
|
||||
"cloud": {
|
||||
"data": {
|
||||
"cloud_username": "Cloud username",
|
||||
"cloud_password": "Cloud password",
|
||||
"cloud_country": "Cloud server country",
|
||||
"cloud_username": "[%key:common::config_flow::data::username%]",
|
||||
"cloud_password": "[%key:common::config_flow::data::password%]",
|
||||
"cloud_country": "Server region",
|
||||
"manual": "Configure manually (not recommended)"
|
||||
},
|
||||
"description": "Log in to the Xiaomi Miio cloud, see https://www.openhab.org/addons/bindings/miio/#country-servers for the cloud server to use."
|
||||
"description": "Log in to Xiaomi Home, see https://www.openhab.org/addons/bindings/miio/#country-servers for the server region to use."
|
||||
},
|
||||
"select": {
|
||||
"data": {
|
||||
"select_device": "Miio device"
|
||||
"select_device": "[%key:common::config_flow::data::device%]"
|
||||
},
|
||||
"description": "Select the Xiaomi Miio device to set up."
|
||||
"description": "Select the Xiaomi Home device to set up."
|
||||
},
|
||||
"manual": {
|
||||
"data": {
|
||||
@@ -58,7 +58,7 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"cloud_subdevices": "Use cloud to get connected subdevices"
|
||||
"cloud_subdevices": "Use Xiaomi Home service to get connected subdevices"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,7 +331,7 @@
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": "Name of the Xiaomi Miio entity."
|
||||
"description": "Name of the Xiaomi Home entity."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"zha",
|
||||
"universal_silabs_flasher"
|
||||
],
|
||||
"requirements": ["zha==0.0.59"],
|
||||
"requirements": ["zha==0.0.60"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "10C4",
|
||||
|
||||
@@ -896,6 +896,7 @@ DISCOVERY_SCHEMAS = [
|
||||
writeable=False,
|
||||
),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# generic text sensors
|
||||
ZWaveDiscoverySchema(
|
||||
@@ -932,6 +933,7 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
data_template=NumericSensorDataTemplate(),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# Meter sensors for Meter CC
|
||||
ZWaveDiscoverySchema(
|
||||
@@ -957,6 +959,7 @@ DISCOVERY_SCHEMAS = [
|
||||
writeable=True,
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# button for Indicator CC
|
||||
ZWaveDiscoverySchema(
|
||||
@@ -980,6 +983,7 @@ DISCOVERY_SCHEMAS = [
|
||||
writeable=True,
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# binary switch
|
||||
# barrier operator signaling states
|
||||
@@ -1184,6 +1188,7 @@ DISCOVERY_SCHEMAS = [
|
||||
any_available_states={(0, "idle")},
|
||||
),
|
||||
allow_multi=True,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# event
|
||||
# stateful = False
|
||||
|
||||
@@ -7475,7 +7475,7 @@
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Xiaomi Miio"
|
||||
"name": "Xiaomi Home"
|
||||
},
|
||||
"xiaomi_tv": {
|
||||
"integration_type": "hub",
|
||||
|
||||
@@ -1020,15 +1020,11 @@ class MediaSelector(Selector[MediaSelectorConfig]):
|
||||
|
||||
selector_type = "media"
|
||||
|
||||
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional("accept"): [str],
|
||||
}
|
||||
)
|
||||
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
# If accept is set, the entity_id field will not be present
|
||||
vol.Optional("entity_id"): cv.entity_id_or_uuid,
|
||||
# Although marked as optional in frontend, this field is required
|
||||
vol.Required("entity_id"): cv.entity_id_or_uuid,
|
||||
# Although marked as optional in frontend, this field is required
|
||||
vol.Required("media_content_id"): str,
|
||||
# Although marked as optional in frontend, this field is required
|
||||
@@ -1117,9 +1113,23 @@ class NumberSelector(Selector[NumberSelectorConfig]):
|
||||
return value
|
||||
|
||||
|
||||
class ObjectSelectorField(TypedDict):
|
||||
"""Class to represent an object selector fields dict."""
|
||||
|
||||
label: str
|
||||
required: bool
|
||||
selector: dict[str, Any]
|
||||
|
||||
|
||||
class ObjectSelectorConfig(BaseSelectorConfig):
|
||||
"""Class to represent an object selector config."""
|
||||
|
||||
fields: dict[str, ObjectSelectorField]
|
||||
multiple: bool
|
||||
label_field: str
|
||||
description_field: bool
|
||||
translation_key: str
|
||||
|
||||
|
||||
@SELECTORS.register("object")
|
||||
class ObjectSelector(Selector[ObjectSelectorConfig]):
|
||||
@@ -1127,7 +1137,21 @@ class ObjectSelector(Selector[ObjectSelectorConfig]):
|
||||
|
||||
selector_type = "object"
|
||||
|
||||
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
|
||||
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional("fields"): {
|
||||
str: {
|
||||
vol.Required("selector"): dict,
|
||||
vol.Optional("required"): bool,
|
||||
vol.Optional("label"): str,
|
||||
}
|
||||
},
|
||||
vol.Optional("multiple", default=False): bool,
|
||||
vol.Optional("label_field"): str,
|
||||
vol.Optional("description_field"): str,
|
||||
vol.Optional("translation_key"): str,
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, config: ObjectSelectorConfig | None = None) -> None:
|
||||
"""Instantiate a selector."""
|
||||
|
||||
@@ -682,9 +682,12 @@ def _load_services_file(hass: HomeAssistant, integration: Integration) -> JSON_T
|
||||
|
||||
def _load_services_files(
|
||||
hass: HomeAssistant, integrations: Iterable[Integration]
|
||||
) -> list[JSON_TYPE]:
|
||||
) -> dict[str, JSON_TYPE]:
|
||||
"""Load service files for multiple integrations."""
|
||||
return [_load_services_file(hass, integration) for integration in integrations]
|
||||
return {
|
||||
integration.domain: _load_services_file(hass, integration)
|
||||
for integration in integrations
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
@@ -715,7 +718,6 @@ async def async_get_all_descriptions(
|
||||
for service_name in services_by_domain
|
||||
}
|
||||
# If we have a complete cache, check if it is still valid
|
||||
all_cache: tuple[set[tuple[str, str]], dict[str, dict[str, Any]]] | None
|
||||
if all_cache := hass.data.get(ALL_SERVICE_DESCRIPTIONS_CACHE):
|
||||
previous_all_services, previous_descriptions_cache = all_cache
|
||||
# If the services are the same, we can return the cache
|
||||
@@ -741,13 +743,16 @@ async def async_get_all_descriptions(
|
||||
continue
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(int_or_exc, Exception)
|
||||
_LOGGER.error("Failed to load integration: %s", domain, exc_info=int_or_exc)
|
||||
_LOGGER.error(
|
||||
"Failed to load services.yaml for integration: %s",
|
||||
domain,
|
||||
exc_info=int_or_exc,
|
||||
)
|
||||
|
||||
if integrations:
|
||||
contents = await hass.async_add_executor_job(
|
||||
loaded = await hass.async_add_executor_job(
|
||||
_load_services_files, hass, integrations
|
||||
)
|
||||
loaded = dict(zip(domains_with_missing_services, contents, strict=False))
|
||||
|
||||
# Load translations for all service domains
|
||||
translations = await translation.async_get_translations(
|
||||
@@ -770,7 +775,7 @@ async def async_get_all_descriptions(
|
||||
# Cache missing descriptions
|
||||
domain_yaml = loaded.get(domain) or {}
|
||||
# The YAML may be empty for dynamically defined
|
||||
# services (ie shell_command) that never call
|
||||
# services (e.g. shell_command) that never call
|
||||
# service.async_set_service_schema for the dynamic
|
||||
# service
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ fnv-hash-fast==1.5.0
|
||||
go2rtc-client==0.2.1
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==3.49.0
|
||||
hass-nabucasa==0.101.0
|
||||
hass-nabucasa==0.102.0
|
||||
hassil==2.2.3
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20250531.3
|
||||
|
||||
@@ -53,7 +53,7 @@ dependencies = [
|
||||
"ha-ffmpeg==3.2.2",
|
||||
# hass-nabucasa is imported by helpers which don't depend on the cloud
|
||||
# integration
|
||||
"hass-nabucasa==0.101.0",
|
||||
"hass-nabucasa==0.102.0",
|
||||
# hassil is indirectly imported from onboarding via the import chain
|
||||
# onboarding->cloud->assist_pipeline->conversation->hassil. Onboarding needs
|
||||
# to be setup in stage 0, but we don't want to also promote cloud with all its
|
||||
|
||||
2
requirements.txt
generated
2
requirements.txt
generated
@@ -24,7 +24,7 @@ ciso8601==2.3.2
|
||||
cronsim==2.6
|
||||
fnv-hash-fast==1.5.0
|
||||
ha-ffmpeg==3.2.2
|
||||
hass-nabucasa==0.101.0
|
||||
hass-nabucasa==0.102.0
|
||||
hassil==2.2.3
|
||||
httpx==0.28.1
|
||||
home-assistant-bluetooth==1.13.1
|
||||
|
||||
32
requirements_all.txt
generated
32
requirements_all.txt
generated
@@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12
|
||||
aioairzone==1.0.0
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==3.1.4
|
||||
aioamazondevices==3.1.12
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -201,7 +201,7 @@ aioaseko==1.0.0
|
||||
aioasuswrt==1.4.0
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2025.5.1
|
||||
aioautomower==2025.6.0
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==2.2.1
|
||||
@@ -244,7 +244,7 @@ aioelectricitymaps==0.4.0
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==32.2.1
|
||||
aioesphomeapi==32.2.4
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@@ -265,7 +265,7 @@ aioharmony==0.5.2
|
||||
aiohasupervisor==0.3.1
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
aiohomeconnect==0.17.1
|
||||
aiohomeconnect==0.18.0
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==3.2.15
|
||||
@@ -369,7 +369,7 @@ aioridwell==2024.01.0
|
||||
aioruckus==0.42
|
||||
|
||||
# homeassistant.components.russound_rio
|
||||
aiorussound==4.5.2
|
||||
aiorussound==4.6.0
|
||||
|
||||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
@@ -429,7 +429,7 @@ aiowaqi==3.1.0
|
||||
aiowatttime==0.1.1
|
||||
|
||||
# homeassistant.components.webdav
|
||||
aiowebdav2==0.4.5
|
||||
aiowebdav2==0.4.6
|
||||
|
||||
# homeassistant.components.webostv
|
||||
aiowebostv==0.7.3
|
||||
@@ -1124,7 +1124,7 @@ habiticalib==0.4.0
|
||||
habluetooth==3.49.0
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.101.0
|
||||
hass-nabucasa==0.102.0
|
||||
|
||||
# homeassistant.components.splunk
|
||||
hass-splunk==0.1.1
|
||||
@@ -1161,7 +1161,7 @@ hole==0.8.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.74
|
||||
holidays==0.75
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250531.3
|
||||
@@ -1203,7 +1203,7 @@ ibmiotf==0.3.4
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==10.0.0
|
||||
ical==10.0.4
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.1.0
|
||||
@@ -1505,7 +1505,7 @@ nexia==2.10.0
|
||||
nextcloudmonitor==1.5.1
|
||||
|
||||
# homeassistant.components.discord
|
||||
nextcord==2.6.0
|
||||
nextcord==3.1.0
|
||||
|
||||
# homeassistant.components.nextdns
|
||||
nextdns==4.0.0
|
||||
@@ -1632,7 +1632,7 @@ orvibo==1.1.2
|
||||
ourgroceries==1.5.4
|
||||
|
||||
# homeassistant.components.ovo_energy
|
||||
ovoenergy==2.0.0
|
||||
ovoenergy==2.0.1
|
||||
|
||||
# homeassistant.components.p1_monitor
|
||||
p1monitor==3.1.0
|
||||
@@ -2096,7 +2096,7 @@ pykwb==0.0.8
|
||||
pylacrosse==0.4
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
pylamarzocco==2.0.8
|
||||
pylamarzocco==2.0.9
|
||||
|
||||
# homeassistant.components.lastfm
|
||||
pylast==5.1.0
|
||||
@@ -2338,10 +2338,10 @@ pysma==0.7.5
|
||||
pysmappee==0.2.29
|
||||
|
||||
# homeassistant.components.smarla
|
||||
pysmarlaapi==0.8.2
|
||||
pysmarlaapi==0.9.0
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.2.4
|
||||
pysmartthings==3.2.5
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.2
|
||||
@@ -2987,7 +2987,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==7.13.0
|
||||
uiprotect==7.14.0
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
@@ -3180,7 +3180,7 @@ zeroconf==0.147.0
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==0.0.59
|
||||
zha==0.0.60
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong-hong-hvac==1.0.13
|
||||
|
||||
32
requirements_test_all.txt
generated
32
requirements_test_all.txt
generated
@@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12
|
||||
aioairzone==1.0.0
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==3.1.4
|
||||
aioamazondevices==3.1.12
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -189,7 +189,7 @@ aioaseko==1.0.0
|
||||
aioasuswrt==1.4.0
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2025.5.1
|
||||
aioautomower==2025.6.0
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==2.2.1
|
||||
@@ -232,7 +232,7 @@ aioelectricitymaps==0.4.0
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==32.2.1
|
||||
aioesphomeapi==32.2.4
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@@ -250,7 +250,7 @@ aioharmony==0.5.2
|
||||
aiohasupervisor==0.3.1
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
aiohomeconnect==0.17.1
|
||||
aiohomeconnect==0.18.0
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==3.2.15
|
||||
@@ -351,7 +351,7 @@ aioridwell==2024.01.0
|
||||
aioruckus==0.42
|
||||
|
||||
# homeassistant.components.russound_rio
|
||||
aiorussound==4.5.2
|
||||
aiorussound==4.6.0
|
||||
|
||||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
@@ -411,7 +411,7 @@ aiowaqi==3.1.0
|
||||
aiowatttime==0.1.1
|
||||
|
||||
# homeassistant.components.webdav
|
||||
aiowebdav2==0.4.5
|
||||
aiowebdav2==0.4.6
|
||||
|
||||
# homeassistant.components.webostv
|
||||
aiowebostv==0.7.3
|
||||
@@ -982,7 +982,7 @@ habiticalib==0.4.0
|
||||
habluetooth==3.49.0
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.101.0
|
||||
hass-nabucasa==0.102.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
hassil==2.2.3
|
||||
@@ -1007,7 +1007,7 @@ hole==0.8.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.74
|
||||
holidays==0.75
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250531.3
|
||||
@@ -1040,7 +1040,7 @@ ibeacon-ble==1.2.0
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==10.0.0
|
||||
ical==10.0.4
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.1.0
|
||||
@@ -1285,7 +1285,7 @@ nexia==2.10.0
|
||||
nextcloudmonitor==1.5.1
|
||||
|
||||
# homeassistant.components.discord
|
||||
nextcord==2.6.0
|
||||
nextcord==3.1.0
|
||||
|
||||
# homeassistant.components.nextdns
|
||||
nextdns==4.0.0
|
||||
@@ -1379,7 +1379,7 @@ oralb-ble==0.17.6
|
||||
ourgroceries==1.5.4
|
||||
|
||||
# homeassistant.components.ovo_energy
|
||||
ovoenergy==2.0.0
|
||||
ovoenergy==2.0.1
|
||||
|
||||
# homeassistant.components.p1_monitor
|
||||
p1monitor==3.1.0
|
||||
@@ -1738,7 +1738,7 @@ pykrakenapi==0.1.8
|
||||
pykulersky==0.5.8
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
pylamarzocco==2.0.8
|
||||
pylamarzocco==2.0.9
|
||||
|
||||
# homeassistant.components.lastfm
|
||||
pylast==5.1.0
|
||||
@@ -1938,10 +1938,10 @@ pysma==0.7.5
|
||||
pysmappee==0.2.29
|
||||
|
||||
# homeassistant.components.smarla
|
||||
pysmarlaapi==0.8.2
|
||||
pysmarlaapi==0.9.0
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.2.4
|
||||
pysmartthings==3.2.5
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.2
|
||||
@@ -2458,7 +2458,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==7.13.0
|
||||
uiprotect==7.14.0
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
@@ -2621,7 +2621,7 @@ zeroconf==0.147.0
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==0.0.59
|
||||
zha==0.0.60
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.63.0
|
||||
|
||||
@@ -480,7 +480,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"hko",
|
||||
"hlk_sw16",
|
||||
"holiday",
|
||||
"home_connect",
|
||||
"homekit",
|
||||
"homekit_controller",
|
||||
"homematic",
|
||||
@@ -1528,7 +1527,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"hko",
|
||||
"hlk_sw16",
|
||||
"holiday",
|
||||
"home_connect",
|
||||
"homekit",
|
||||
"homekit_controller",
|
||||
"homematic",
|
||||
|
||||
@@ -245,11 +245,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
|
||||
# opower > arrow > types-python-dateutil
|
||||
"arrow": {"types-python-dateutil"}
|
||||
},
|
||||
"ovo_energy": {
|
||||
# https://github.com/timmo001/ovoenergy/issues/132
|
||||
# ovoenergy > incremental > setuptools
|
||||
"incremental": {"setuptools"}
|
||||
},
|
||||
"pi_hole": {"hole": {"async-timeout"}},
|
||||
"pvpc_hourly_pricing": {"aiopvpc": {"async-timeout"}},
|
||||
"remote_rpi_gpio": {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from enum import Enum
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import cover
|
||||
from homeassistant.components.cover import CoverState
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, SERVICE_TOGGLE
|
||||
@@ -13,11 +11,7 @@ from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import MockCover
|
||||
|
||||
from tests.common import (
|
||||
MockEntityPlatform,
|
||||
help_test_all,
|
||||
setup_test_component_platform,
|
||||
)
|
||||
from tests.common import help_test_all, setup_test_component_platform
|
||||
|
||||
|
||||
async def test_services(
|
||||
@@ -159,24 +153,3 @@ def _create_tuples(enum: type[Enum], constant_prefix: str) -> list[tuple[Enum, s
|
||||
def test_all() -> None:
|
||||
"""Test module.__all__ is correctly set."""
|
||||
help_test_all(cover)
|
||||
|
||||
|
||||
def test_deprecated_supported_features_ints(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test deprecated supported features ints."""
|
||||
|
||||
class MockCoverEntity(cover.CoverEntity):
|
||||
_attr_supported_features = 1
|
||||
|
||||
entity = MockCoverEntity()
|
||||
entity.hass = hass
|
||||
entity.platform = MockEntityPlatform(hass)
|
||||
assert entity.supported_features is cover.CoverEntityFeature(1)
|
||||
assert "MockCoverEntity" in caplog.text
|
||||
assert "is using deprecated supported features values" in caplog.text
|
||||
assert "Instead it should use" in caplog.text
|
||||
assert "CoverEntityFeature.OPEN" in caplog.text
|
||||
caplog.clear()
|
||||
assert entity.supported_features is cover.CoverEntityFeature(1)
|
||||
assert "is using deprecated supported features values" not in caplog.text
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
@@ -19,7 +18,6 @@ from .mocks import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_binary_sensor(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
@@ -58,7 +56,6 @@ async def test_binary_sensor(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_remote_control(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
@@ -99,7 +96,6 @@ async def test_remote_control(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_disabled(hass: HomeAssistant) -> None:
|
||||
"""Test setup of a disabled device."""
|
||||
entry = configure_integration(hass)
|
||||
@@ -113,7 +109,6 @@ async def test_disabled(hass: HomeAssistant) -> None:
|
||||
assert hass.states.get(f"{BINARY_SENSOR_DOMAIN}.test_door") is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_remove_from_hass(hass: HomeAssistant) -> None:
|
||||
"""Test removing entity."""
|
||||
entry = configure_integration(hass)
|
||||
|
||||
@@ -66,44 +66,6 @@ async def test_form_already_configured(hass: HomeAssistant) -> None:
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_form_advanced_options(hass: HomeAssistant) -> None:
|
||||
"""Test if we get the advanced options if user has enabled it."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER, "show_advanced_options": True},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.devolo_home_control.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
patch(
|
||||
"homeassistant.components.devolo_home_control.Mydevolo.uuid",
|
||||
return_value="123456",
|
||||
),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "devolo Home Control"
|
||||
assert result2["data"] == {
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
}
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_zeroconf(hass: HomeAssistant) -> None:
|
||||
"""Test that the zeroconf confirmation form is served."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Tests for the devolo Home Control diagnostics."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
@@ -19,7 +19,6 @@ from .mocks import HomeControlMock, HomeControlMockBinarySensor
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_setup_entry(hass: HomeAssistant) -> None:
|
||||
"""Test setup entry."""
|
||||
entry = configure_integration(hass)
|
||||
@@ -44,7 +43,6 @@ async def test_setup_entry_maintenance(hass: HomeAssistant) -> None:
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_setup_gateway_offline(hass: HomeAssistant) -> None:
|
||||
"""Test setup entry fails on gateway offline."""
|
||||
entry = configure_integration(hass)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN
|
||||
@@ -14,7 +13,6 @@ from . import configure_integration
|
||||
from .mocks import HomeControlMock, HomeControlMockSiren
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_siren(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
@@ -45,7 +43,6 @@ async def test_siren(
|
||||
assert hass.states.get(f"{SIREN_DOMAIN}.test").state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_siren_switching(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
@@ -98,7 +95,6 @@ async def test_siren_switching(
|
||||
property_set.assert_called_once_with(0)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_siren_change_default_tone(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
@@ -130,7 +126,6 @@ async def test_siren_change_default_tone(
|
||||
property_set.assert_called_once_with(2)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_remove_from_hass(hass: HomeAssistant) -> None:
|
||||
"""Test removing entity."""
|
||||
entry = configure_integration(hass)
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'band': '5 GHz',
|
||||
'friendly_name': 'AA:BB:CC:DD:EE:FF',
|
||||
'mac': 'AA:BB:CC:DD:EE:FF',
|
||||
'source_type': <SourceType.ROUTER: 'router'>,
|
||||
'wifi': 'Main',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'device_tracker.devolo_home_network_1234567890_aa_bb_cc_dd_ee_ff',
|
||||
'entity_id': 'device_tracker.aa_bb_cc_dd_ee_ff',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
|
||||
@@ -17,13 +17,12 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import configure_integration
|
||||
from .const import CONNECTED_STATIONS, DISCOVERY_INFO, NO_CONNECTED_STATIONS
|
||||
from .const import CONNECTED_STATIONS, NO_CONNECTED_STATIONS
|
||||
from .mock import MockDevice
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
STATION = CONNECTED_STATIONS[0]
|
||||
SERIAL = DISCOVERY_INFO.properties["SN"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@@ -35,9 +34,7 @@ async def test_device_tracker(
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test device tracker states."""
|
||||
state_key = (
|
||||
f"{PLATFORM}.{DOMAIN}_{SERIAL}_{STATION.mac_address.lower().replace(':', '_')}"
|
||||
)
|
||||
state_key = f"{PLATFORM}.{STATION.mac_address.lower().replace(':', '_')}"
|
||||
entry = configure_integration(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -77,14 +74,12 @@ async def test_restoring_clients(
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test restoring existing device_tracker entities."""
|
||||
state_key = (
|
||||
f"{PLATFORM}.{DOMAIN}_{SERIAL}_{STATION.mac_address.lower().replace(':', '_')}"
|
||||
)
|
||||
state_key = f"{PLATFORM}.{STATION.mac_address.lower().replace(':', '_')}"
|
||||
entry = configure_integration(hass)
|
||||
entity_registry.async_get_or_create(
|
||||
PLATFORM,
|
||||
DOMAIN,
|
||||
f"{SERIAL}_{STATION.mac_address}",
|
||||
f"{STATION.mac_address}",
|
||||
config_entry=entry,
|
||||
)
|
||||
|
||||
|
||||
@@ -240,6 +240,17 @@ async def test_pipeline_api_audio(
|
||||
)
|
||||
assert satellite.state == AssistSatelliteState.PROCESSING
|
||||
|
||||
event_callback(
|
||||
PipelineEvent(
|
||||
type=PipelineEventType.INTENT_PROGRESS,
|
||||
data={"tts_start_streaming": True},
|
||||
)
|
||||
)
|
||||
assert mock_client.send_voice_assistant_event.call_args_list[-1].args == (
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS,
|
||||
{"tts_start_streaming": True},
|
||||
)
|
||||
|
||||
event_callback(
|
||||
PipelineEvent(
|
||||
type=PipelineEventType.INTENT_END,
|
||||
|
||||
@@ -150,10 +150,10 @@ async def test_sensor(
|
||||
duration = hass.states.get("sensor.test_duration")
|
||||
assert duration.attributes.get("unit_of_measurement") == UnitOfTime.MINUTES
|
||||
assert duration.attributes.get(ATTR_ICON) == icon
|
||||
assert duration.state == "26"
|
||||
assert duration.state == "26.1833333333333"
|
||||
|
||||
assert float(hass.states.get("sensor.test_distance").state) == pytest.approx(13.682)
|
||||
assert hass.states.get("sensor.test_duration_in_traffic").state == "30"
|
||||
assert hass.states.get("sensor.test_duration_in_traffic").state == "29.6"
|
||||
assert hass.states.get("sensor.test_origin").state == "22nd St NW"
|
||||
assert (
|
||||
hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE)
|
||||
@@ -501,13 +501,13 @@ async def test_restore_state(hass: HomeAssistant) -> None:
|
||||
"1234",
|
||||
attributes={
|
||||
ATTR_LAST_RESET: last_reset,
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.MINUTES,
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.SECONDS,
|
||||
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
),
|
||||
{
|
||||
"native_value": 1234,
|
||||
"native_unit_of_measurement": UnitOfTime.MINUTES,
|
||||
"native_unit_of_measurement": UnitOfTime.SECONDS,
|
||||
"icon": "mdi:car",
|
||||
"last_reset": last_reset,
|
||||
},
|
||||
@@ -518,13 +518,13 @@ async def test_restore_state(hass: HomeAssistant) -> None:
|
||||
"5678",
|
||||
attributes={
|
||||
ATTR_LAST_RESET: last_reset,
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.MINUTES,
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.SECONDS,
|
||||
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
),
|
||||
{
|
||||
"native_value": 5678,
|
||||
"native_unit_of_measurement": UnitOfTime.MINUTES,
|
||||
"native_unit_of_measurement": UnitOfTime.SECONDS,
|
||||
"icon": "mdi:car",
|
||||
"last_reset": last_reset,
|
||||
},
|
||||
@@ -596,12 +596,12 @@ async def test_restore_state(hass: HomeAssistant) -> None:
|
||||
|
||||
# restore from cache
|
||||
state = hass.states.get("sensor.test_duration")
|
||||
assert state.state == "1234"
|
||||
assert state.state == "20.5666666666667"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.MINUTES
|
||||
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
|
||||
|
||||
state = hass.states.get("sensor.test_duration_in_traffic")
|
||||
assert state.state == "5678"
|
||||
assert state.state == "94.6333333333333"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.MINUTES
|
||||
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
|
||||
|
||||
@@ -799,10 +799,12 @@ async def test_multiple_sections(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
duration = hass.states.get("sensor.test_duration")
|
||||
assert duration.state == "18"
|
||||
assert duration.state == "18.4833333333333"
|
||||
|
||||
assert float(hass.states.get("sensor.test_distance").state) == pytest.approx(3.583)
|
||||
assert hass.states.get("sensor.test_duration_in_traffic").state == "18"
|
||||
assert (
|
||||
hass.states.get("sensor.test_duration_in_traffic").state == "18.4833333333333"
|
||||
)
|
||||
assert hass.states.get("sensor.test_origin").state == "Chemin de Halage"
|
||||
assert (
|
||||
hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE)
|
||||
|
||||
@@ -243,7 +243,14 @@
|
||||
"1/29/1": [3, 29, 47, 144, 145, 156],
|
||||
"1/29/2": [],
|
||||
"1/29/3": [],
|
||||
"1/29/4": [],
|
||||
"1/29/4": [
|
||||
{
|
||||
"0": null,
|
||||
"1": 15,
|
||||
"2": 2,
|
||||
"3": "Solar"
|
||||
}
|
||||
],
|
||||
"1/29/65532": 0,
|
||||
"1/29/65533": 2,
|
||||
"1/29/65528": [],
|
||||
|
||||
@@ -152,7 +152,9 @@ def test_support_properties(hass: HomeAssistant, property_suffix: str) -> None:
|
||||
entity4 = MediaPlayerEntity()
|
||||
entity4.hass = hass
|
||||
entity4.platform = MockEntityPlatform(hass)
|
||||
entity4._attr_supported_features = all_features - feature
|
||||
entity4._attr_supported_features = media_player.MediaPlayerEntityFeature(
|
||||
all_features - feature
|
||||
)
|
||||
|
||||
assert getattr(entity1, f"support_{property_suffix}") is False
|
||||
assert getattr(entity2, f"support_{property_suffix}") is True
|
||||
@@ -652,27 +654,3 @@ async def test_get_async_get_browse_image_quoting(
|
||||
url = player.get_browse_image_url("album", media_content_id)
|
||||
await client.get(url)
|
||||
mock_browse_image.assert_called_with("album", media_content_id, None)
|
||||
|
||||
|
||||
def test_deprecated_supported_features_ints(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test deprecated supported features ints."""
|
||||
|
||||
class MockMediaPlayerEntity(MediaPlayerEntity):
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Return supported features."""
|
||||
return 1
|
||||
|
||||
entity = MockMediaPlayerEntity()
|
||||
entity.hass = hass
|
||||
entity.platform = MockEntityPlatform(hass)
|
||||
assert entity.supported_features_compat is MediaPlayerEntityFeature(1)
|
||||
assert "MockMediaPlayerEntity" in caplog.text
|
||||
assert "is using deprecated supported features values" in caplog.text
|
||||
assert "Instead it should use" in caplog.text
|
||||
assert "MediaPlayerEntityFeature.PAUSE" in caplog.text
|
||||
caplog.clear()
|
||||
assert entity.supported_features_compat is MediaPlayerEntityFeature(1)
|
||||
assert "is using deprecated supported features values" not in caplog.text
|
||||
|
||||
@@ -898,42 +898,12 @@ async def test_invalid_unit_of_measurement(
|
||||
"The unit of measurement `ppm` is not valid together with device class `energy`"
|
||||
in caplog.text
|
||||
)
|
||||
# A repair issue was logged
|
||||
# A repair issue was logged for the failing YAML config
|
||||
assert len(events) == 1
|
||||
assert events[0].data["issue_id"] == "sensor.test"
|
||||
# Assert the sensor works
|
||||
async_fire_mqtt_message(hass, "test-topic", "100")
|
||||
await hass.async_block_till_done()
|
||||
assert events[0].data["domain"] == mqtt.DOMAIN
|
||||
# Assert the sensor is not created
|
||||
state = hass.states.get("sensor.test")
|
||||
assert state is not None
|
||||
assert state.state == "100"
|
||||
|
||||
caplog.clear()
|
||||
|
||||
discovery_payload = {
|
||||
"name": "bla",
|
||||
"state_topic": "test-topic2",
|
||||
"device_class": "temperature",
|
||||
"unit_of_measurement": "C",
|
||||
}
|
||||
# Now discover an other invalid sensor
|
||||
async_fire_mqtt_message(
|
||||
hass, "homeassistant/sensor/bla/config", json.dumps(discovery_payload)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert (
|
||||
"The unit of measurement `C` is not valid together with device class `temperature`"
|
||||
in caplog.text
|
||||
)
|
||||
# Assert the sensor works
|
||||
async_fire_mqtt_message(hass, "test-topic2", "21")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.bla")
|
||||
assert state is not None
|
||||
assert state.state == "21"
|
||||
|
||||
# No new issue was registered for the discovered entity
|
||||
assert len(events) == 1
|
||||
assert state is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -62,6 +62,108 @@ def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
def _init_host_mock(host_mock: MagicMock) -> None:
|
||||
host_mock.get_host_data = AsyncMock(return_value=None)
|
||||
host_mock.get_states = AsyncMock(return_value=None)
|
||||
host_mock.get_state = AsyncMock()
|
||||
host_mock.check_new_firmware = AsyncMock(return_value=False)
|
||||
host_mock.subscribe = AsyncMock()
|
||||
host_mock.unsubscribe = AsyncMock(return_value=True)
|
||||
host_mock.logout = AsyncMock(return_value=True)
|
||||
host_mock.reboot = AsyncMock()
|
||||
host_mock.set_ptz_command = AsyncMock()
|
||||
host_mock.get_motion_state_all_ch = AsyncMock(return_value=False)
|
||||
host_mock.get_stream_source = AsyncMock()
|
||||
host_mock.get_snapshot = AsyncMock()
|
||||
host_mock.get_encoding = AsyncMock(return_value="h264")
|
||||
host_mock.ONVIF_event_callback = AsyncMock()
|
||||
host_mock.is_nvr = True
|
||||
host_mock.is_hub = False
|
||||
host_mock.mac_address = TEST_MAC
|
||||
host_mock.uid = TEST_UID
|
||||
host_mock.onvif_enabled = True
|
||||
host_mock.rtmp_enabled = True
|
||||
host_mock.rtsp_enabled = True
|
||||
host_mock.nvr_name = TEST_NVR_NAME
|
||||
host_mock.port = TEST_PORT
|
||||
host_mock.use_https = TEST_USE_HTTPS
|
||||
host_mock.is_admin = True
|
||||
host_mock.user_level = "admin"
|
||||
host_mock.protocol = "rtsp"
|
||||
host_mock.channels = [0]
|
||||
host_mock.stream_channels = [0]
|
||||
host_mock.new_devices = False
|
||||
host_mock.sw_version_update_required = False
|
||||
host_mock.hardware_version = "IPC_00000"
|
||||
host_mock.sw_version = "v1.0.0.0.0.0000"
|
||||
host_mock.sw_upload_progress.return_value = 100
|
||||
host_mock.manufacturer = "Reolink"
|
||||
host_mock.model = TEST_HOST_MODEL
|
||||
host_mock.supported.return_value = True
|
||||
host_mock.item_number.return_value = TEST_ITEM_NUMBER
|
||||
host_mock.camera_model.return_value = TEST_CAM_MODEL
|
||||
host_mock.camera_name.return_value = TEST_NVR_NAME
|
||||
host_mock.camera_hardware_version.return_value = "IPC_00001"
|
||||
host_mock.camera_sw_version.return_value = "v1.1.0.0.0.0000"
|
||||
host_mock.camera_sw_version_update_required.return_value = False
|
||||
host_mock.camera_uid.return_value = TEST_UID_CAM
|
||||
host_mock.camera_online.return_value = True
|
||||
host_mock.channel_for_uid.return_value = 0
|
||||
host_mock.firmware_update_available.return_value = False
|
||||
host_mock.session_active = True
|
||||
host_mock.timeout = 60
|
||||
host_mock.renewtimer.return_value = 600
|
||||
host_mock.wifi_connection = False
|
||||
host_mock.wifi_signal = None
|
||||
host_mock.whiteled_mode_list.return_value = []
|
||||
host_mock.zoom_range.return_value = {
|
||||
"zoom": {"pos": {"min": 0, "max": 100}},
|
||||
"focus": {"pos": {"min": 0, "max": 100}},
|
||||
}
|
||||
host_mock.capabilities = {"Host": ["RTSP"], "0": ["motion_detection"]}
|
||||
host_mock.checked_api_versions = {"GetEvents": 1}
|
||||
host_mock.abilities = {"abilityChn": [{"aiTrack": {"permit": 0, "ver": 0}}]}
|
||||
host_mock.get_raw_host_data.return_value = (
|
||||
"{'host':'TEST_RESPONSE','channel':'TEST_RESPONSE'}"
|
||||
)
|
||||
|
||||
# enums
|
||||
host_mock.whiteled_mode.return_value = 1
|
||||
host_mock.whiteled_mode_list.return_value = ["off", "auto"]
|
||||
host_mock.doorbell_led.return_value = "Off"
|
||||
host_mock.doorbell_led_list.return_value = ["stayoff", "auto"]
|
||||
host_mock.auto_track_method.return_value = 3
|
||||
host_mock.daynight_state.return_value = "Black&White"
|
||||
host_mock.hub_alarm_tone_id.return_value = 1
|
||||
host_mock.hub_visitor_tone_id.return_value = 1
|
||||
host_mock.recording_packing_time_list = ["30 Minutes", "60 Minutes"]
|
||||
host_mock.recording_packing_time = "60 Minutes"
|
||||
|
||||
# Baichuan
|
||||
host_mock.baichuan_only = False
|
||||
# Disable tcp push by default for tests
|
||||
host_mock.baichuan.port = TEST_BC_PORT
|
||||
host_mock.baichuan.events_active = False
|
||||
host_mock.baichuan.subscribe_events = AsyncMock()
|
||||
host_mock.baichuan.unsubscribe_events = AsyncMock()
|
||||
host_mock.baichuan.check_subscribe_events = AsyncMock()
|
||||
host_mock.baichuan.get_privacy_mode = AsyncMock()
|
||||
host_mock.baichuan.mac_address.return_value = TEST_MAC_CAM
|
||||
host_mock.baichuan.privacy_mode.return_value = False
|
||||
host_mock.baichuan.day_night_state.return_value = "day"
|
||||
host_mock.baichuan.subscribe_events.side_effect = ReolinkError("Test error")
|
||||
host_mock.baichuan.active_scene = "off"
|
||||
host_mock.baichuan.scene_names = ["off", "home"]
|
||||
host_mock.baichuan.abilities = {
|
||||
0: {"chnID": 0, "aitype": 34615},
|
||||
"Host": {"pushAlarm": 7},
|
||||
}
|
||||
host_mock.baichuan.smart_location_list.return_value = [0]
|
||||
host_mock.baichuan.smart_ai_type_list.return_value = ["people"]
|
||||
host_mock.baichuan.smart_ai_index.return_value = 1
|
||||
host_mock.baichuan.smart_ai_name.return_value = "zone1"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def reolink_connect_class() -> Generator[MagicMock]:
|
||||
"""Mock reolink connection and return both the host_mock and host_mock_class."""
|
||||
@@ -71,97 +173,8 @@ def reolink_connect_class() -> Generator[MagicMock]:
|
||||
) as host_mock_class,
|
||||
):
|
||||
host_mock = host_mock_class.return_value
|
||||
host_mock.get_host_data.return_value = None
|
||||
host_mock.get_states.return_value = None
|
||||
host_mock.supported.return_value = True
|
||||
host_mock.check_new_firmware.return_value = False
|
||||
host_mock.unsubscribe.return_value = True
|
||||
host_mock.logout.return_value = True
|
||||
host_mock.is_nvr = True
|
||||
host_mock.is_hub = False
|
||||
host_mock.mac_address = TEST_MAC
|
||||
host_mock.uid = TEST_UID
|
||||
host_mock.onvif_enabled = True
|
||||
host_mock.rtmp_enabled = True
|
||||
host_mock.rtsp_enabled = True
|
||||
host_mock.nvr_name = TEST_NVR_NAME
|
||||
host_mock.port = TEST_PORT
|
||||
host_mock.use_https = TEST_USE_HTTPS
|
||||
host_mock.is_admin = True
|
||||
host_mock.user_level = "admin"
|
||||
host_mock.protocol = "rtsp"
|
||||
host_mock.channels = [0]
|
||||
host_mock.stream_channels = [0]
|
||||
host_mock.new_devices = False
|
||||
host_mock.sw_version_update_required = False
|
||||
host_mock.hardware_version = "IPC_00000"
|
||||
host_mock.sw_version = "v1.0.0.0.0.0000"
|
||||
host_mock.sw_upload_progress.return_value = 100
|
||||
host_mock.manufacturer = "Reolink"
|
||||
host_mock.model = TEST_HOST_MODEL
|
||||
host_mock.item_number.return_value = TEST_ITEM_NUMBER
|
||||
host_mock.camera_model.return_value = TEST_CAM_MODEL
|
||||
host_mock.camera_name.return_value = TEST_NVR_NAME
|
||||
host_mock.camera_hardware_version.return_value = "IPC_00001"
|
||||
host_mock.camera_sw_version.return_value = "v1.1.0.0.0.0000"
|
||||
host_mock.camera_sw_version_update_required.return_value = False
|
||||
host_mock.camera_uid.return_value = TEST_UID_CAM
|
||||
host_mock.camera_online.return_value = True
|
||||
host_mock.channel_for_uid.return_value = 0
|
||||
host_mock.get_encoding.return_value = "h264"
|
||||
host_mock.firmware_update_available.return_value = False
|
||||
host_mock.session_active = True
|
||||
host_mock.timeout = 60
|
||||
host_mock.renewtimer.return_value = 600
|
||||
host_mock.wifi_connection = False
|
||||
host_mock.wifi_signal = None
|
||||
host_mock.whiteled_mode_list.return_value = []
|
||||
host_mock.zoom_range.return_value = {
|
||||
"zoom": {"pos": {"min": 0, "max": 100}},
|
||||
"focus": {"pos": {"min": 0, "max": 100}},
|
||||
}
|
||||
host_mock.capabilities = {"Host": ["RTSP"], "0": ["motion_detection"]}
|
||||
host_mock.checked_api_versions = {"GetEvents": 1}
|
||||
host_mock.abilities = {"abilityChn": [{"aiTrack": {"permit": 0, "ver": 0}}]}
|
||||
host_mock.get_raw_host_data.return_value = (
|
||||
"{'host':'TEST_RESPONSE','channel':'TEST_RESPONSE'}"
|
||||
)
|
||||
|
||||
reolink_connect.chime_list = []
|
||||
|
||||
# enums
|
||||
host_mock.whiteled_mode.return_value = 1
|
||||
host_mock.whiteled_mode_list.return_value = ["off", "auto"]
|
||||
host_mock.doorbell_led.return_value = "Off"
|
||||
host_mock.doorbell_led_list.return_value = ["stayoff", "auto"]
|
||||
host_mock.auto_track_method.return_value = 3
|
||||
host_mock.daynight_state.return_value = "Black&White"
|
||||
host_mock.hub_alarm_tone_id.return_value = 1
|
||||
host_mock.hub_visitor_tone_id.return_value = 1
|
||||
host_mock.recording_packing_time_list = ["30 Minutes", "60 Minutes"]
|
||||
host_mock.recording_packing_time = "60 Minutes"
|
||||
|
||||
# Baichuan
|
||||
host_mock.baichuan = create_autospec(Baichuan)
|
||||
host_mock.baichuan_only = False
|
||||
# Disable tcp push by default for tests
|
||||
host_mock.baichuan.port = TEST_BC_PORT
|
||||
host_mock.baichuan.events_active = False
|
||||
host_mock.baichuan.mac_address.return_value = TEST_MAC_CAM
|
||||
host_mock.baichuan.privacy_mode.return_value = False
|
||||
host_mock.baichuan.day_night_state.return_value = "day"
|
||||
host_mock.baichuan.subscribe_events.side_effect = ReolinkError("Test error")
|
||||
host_mock.baichuan.active_scene = "off"
|
||||
host_mock.baichuan.scene_names = ["off", "home"]
|
||||
host_mock.baichuan.abilities = {
|
||||
0: {"chnID": 0, "aitype": 34615},
|
||||
"Host": {"pushAlarm": 7},
|
||||
}
|
||||
host_mock.baichuan.smart_location_list.return_value = [0]
|
||||
host_mock.baichuan.smart_ai_type_list.return_value = ["people"]
|
||||
host_mock.baichuan.smart_ai_index.return_value = 1
|
||||
host_mock.baichuan.smart_ai_name.return_value = "zone1"
|
||||
|
||||
_init_host_mock(host_mock)
|
||||
yield host_mock_class
|
||||
|
||||
|
||||
@@ -173,6 +186,18 @@ def reolink_connect(
|
||||
return reolink_connect_class.return_value
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reolink_host() -> Generator[MagicMock]:
|
||||
"""Mock reolink Host class."""
|
||||
with patch(
|
||||
"homeassistant.components.reolink.host.Host", autospec=False
|
||||
) as host_mock_class:
|
||||
host_mock = host_mock_class.return_value
|
||||
host_mock.baichuan = MagicMock()
|
||||
_init_host_mock(host_mock)
|
||||
yield host_mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reolink_platforms() -> Generator[None]:
|
||||
"""Mock reolink entry setup."""
|
||||
@@ -225,3 +250,26 @@ def test_chime(reolink_connect: MagicMock) -> None:
|
||||
reolink_connect.chime_list = [TEST_CHIME]
|
||||
reolink_connect.chime.return_value = TEST_CHIME
|
||||
return TEST_CHIME
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reolink_chime(reolink_host: MagicMock) -> None:
|
||||
"""Mock a reolink chime."""
|
||||
TEST_CHIME = Chime(
|
||||
host=reolink_host,
|
||||
dev_id=12345678,
|
||||
channel=0,
|
||||
)
|
||||
TEST_CHIME.name = "Test chime"
|
||||
TEST_CHIME.volume = 3
|
||||
TEST_CHIME.connect_state = 2
|
||||
TEST_CHIME.led_state = True
|
||||
TEST_CHIME.event_info = {
|
||||
"md": {"switch": 0, "musicId": 0},
|
||||
"people": {"switch": 0, "musicId": 1},
|
||||
"visitor": {"switch": 1, "musicId": 2},
|
||||
}
|
||||
|
||||
reolink_host.chime_list = [TEST_CHIME]
|
||||
reolink_host.chime.return_value = TEST_CHIME
|
||||
return TEST_CHIME
|
||||
|
||||
@@ -21,11 +21,11 @@ async def test_motion_sensor(
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
) -> None:
|
||||
"""Test binary sensor entity with motion sensor."""
|
||||
reolink_connect.model = TEST_DUO_MODEL
|
||||
reolink_connect.motion_detected.return_value = True
|
||||
reolink_host.model = TEST_DUO_MODEL
|
||||
reolink_host.motion_detected.return_value = True
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.BINARY_SENSOR]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -34,7 +34,7 @@ async def test_motion_sensor(
|
||||
entity_id = f"{Platform.BINARY_SENSOR}.{TEST_NVR_NAME}_motion_lens_0"
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
|
||||
reolink_connect.motion_detected.return_value = False
|
||||
reolink_host.motion_detected.return_value = False
|
||||
freezer.tick(DEVICE_UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
@@ -42,8 +42,8 @@ async def test_motion_sensor(
|
||||
assert hass.states.get(entity_id).state == STATE_OFF
|
||||
|
||||
# test ONVIF webhook callback
|
||||
reolink_connect.motion_detected.return_value = True
|
||||
reolink_connect.ONVIF_event_callback.return_value = [0]
|
||||
reolink_host.motion_detected.return_value = True
|
||||
reolink_host.ONVIF_event_callback.return_value = [0]
|
||||
webhook_id = config_entry.runtime_data.host.webhook_id
|
||||
client = await hass_client_no_auth()
|
||||
await client.post(f"/api/webhook/{webhook_id}", data="test_data")
|
||||
@@ -56,11 +56,11 @@ async def test_smart_ai_sensor(
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
) -> None:
|
||||
"""Test smart ai binary sensor entity."""
|
||||
reolink_connect.model = TEST_HOST_MODEL
|
||||
reolink_connect.baichuan.smart_ai_state.return_value = True
|
||||
reolink_host.model = TEST_HOST_MODEL
|
||||
reolink_host.baichuan.smart_ai_state.return_value = True
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.BINARY_SENSOR]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -69,7 +69,7 @@ async def test_smart_ai_sensor(
|
||||
entity_id = f"{Platform.BINARY_SENSOR}.{TEST_NVR_NAME}_crossline_zone1_person"
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
|
||||
reolink_connect.baichuan.smart_ai_state.return_value = False
|
||||
reolink_host.baichuan.smart_ai_state.return_value = False
|
||||
freezer.tick(DEVICE_UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
@@ -80,7 +80,7 @@ async def test_smart_ai_sensor(
|
||||
async def test_tcp_callback(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
) -> None:
|
||||
"""Test tcp callback using motion sensor."""
|
||||
|
||||
@@ -95,11 +95,11 @@ async def test_tcp_callback(
|
||||
|
||||
callback_mock = callback_mock_class()
|
||||
|
||||
reolink_connect.model = TEST_HOST_MODEL
|
||||
reolink_connect.baichuan.events_active = True
|
||||
reolink_connect.baichuan.subscribe_events.reset_mock(side_effect=True)
|
||||
reolink_connect.baichuan.register_callback = callback_mock.register_callback
|
||||
reolink_connect.motion_detected.return_value = True
|
||||
reolink_host.model = TEST_HOST_MODEL
|
||||
reolink_host.baichuan.events_active = True
|
||||
reolink_host.baichuan.subscribe_events.reset_mock(side_effect=True)
|
||||
reolink_host.baichuan.register_callback = callback_mock.register_callback
|
||||
reolink_host.motion_detected.return_value = True
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.BINARY_SENSOR]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
@@ -110,7 +110,7 @@ async def test_tcp_callback(
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
|
||||
# simulate a TCP push callback
|
||||
reolink_connect.motion_detected.return_value = False
|
||||
reolink_host.motion_detected.return_value = False
|
||||
assert callback_mock.callback_func is not None
|
||||
callback_mock.callback_func()
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from tests.common import MockConfigEntry
|
||||
async def test_button(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
) -> None:
|
||||
"""Test button entity with ptz up."""
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.BUTTON]):
|
||||
@@ -37,9 +37,9 @@ async def test_button(
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
reolink_connect.set_ptz_command.assert_called_once()
|
||||
reolink_host.set_ptz_command.assert_called_once()
|
||||
|
||||
reolink_connect.set_ptz_command.side_effect = ReolinkError("Test error")
|
||||
reolink_host.set_ptz_command.side_effect = ReolinkError("Test error")
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
@@ -48,13 +48,11 @@ async def test_button(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
reolink_connect.set_ptz_command.reset_mock(side_effect=True)
|
||||
|
||||
|
||||
async def test_ptz_move_service(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
) -> None:
|
||||
"""Test ptz_move entity service using PTZ button entity."""
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.BUTTON]):
|
||||
@@ -70,9 +68,9 @@ async def test_ptz_move_service(
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_SPEED: 5},
|
||||
blocking=True,
|
||||
)
|
||||
reolink_connect.set_ptz_command.assert_called_with(0, command="Up", speed=5)
|
||||
reolink_host.set_ptz_command.assert_called_with(0, command="Up", speed=5)
|
||||
|
||||
reolink_connect.set_ptz_command.side_effect = ReolinkError("Test error")
|
||||
reolink_host.set_ptz_command.side_effect = ReolinkError("Test error")
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
@@ -81,14 +79,12 @@ async def test_ptz_move_service(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
reolink_connect.set_ptz_command.reset_mock(side_effect=True)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_host_button(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
) -> None:
|
||||
"""Test host button entity with reboot."""
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.BUTTON]):
|
||||
@@ -104,9 +100,9 @@ async def test_host_button(
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
reolink_connect.reboot.assert_called_once()
|
||||
reolink_host.reboot.assert_called_once()
|
||||
|
||||
reolink_connect.reboot.side_effect = ReolinkError("Test error")
|
||||
reolink_host.reboot.side_effect = ReolinkError("Test error")
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
@@ -114,5 +110,3 @@ async def test_host_button(
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
reolink_connect.reboot.reset_mock(side_effect=True)
|
||||
|
||||
@@ -25,7 +25,7 @@ async def test_camera(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
) -> None:
|
||||
"""Test camera entity with fluent."""
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]):
|
||||
@@ -37,28 +37,26 @@ async def test_camera(
|
||||
assert hass.states.get(entity_id).state == CameraState.IDLE
|
||||
|
||||
# check getting a image from the camera
|
||||
reolink_connect.get_snapshot.return_value = b"image"
|
||||
reolink_host.get_snapshot.return_value = b"image"
|
||||
assert (await async_get_image(hass, entity_id)).content == b"image"
|
||||
|
||||
reolink_connect.get_snapshot.side_effect = ReolinkError("Test error")
|
||||
reolink_host.get_snapshot.side_effect = ReolinkError("Test error")
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await async_get_image(hass, entity_id)
|
||||
|
||||
# check getting the stream source
|
||||
assert await async_get_stream_source(hass, entity_id) is not None
|
||||
|
||||
reolink_connect.get_snapshot.reset_mock(side_effect=True)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_camera_no_stream_source(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
) -> None:
|
||||
"""Test camera entity with no stream source."""
|
||||
reolink_connect.model = TEST_DUO_MODEL
|
||||
reolink_connect.get_stream_source.return_value = None
|
||||
reolink_host.model = TEST_DUO_MODEL
|
||||
reolink_host.get_stream_source.return_value = None
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
@@ -15,8 +15,8 @@ from tests.typing import ClientSessionGenerator
|
||||
async def test_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
reolink_connect: MagicMock,
|
||||
test_chime: Chime,
|
||||
reolink_host: MagicMock,
|
||||
reolink_chime: Chime,
|
||||
config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
|
||||
@@ -115,9 +115,11 @@ async def test_webhook_callback(
|
||||
assert hass.states.get(entity_id).state == STATE_OFF
|
||||
|
||||
# test webhook callback success all channels
|
||||
reolink_connect.get_motion_state_all_ch.return_value = True
|
||||
reolink_connect.motion_detected.return_value = True
|
||||
reolink_connect.ONVIF_event_callback.return_value = None
|
||||
await client.post(f"/api/webhook/{webhook_id}")
|
||||
await hass.async_block_till_done()
|
||||
signal_all.assert_called_once()
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
|
||||
@@ -129,6 +131,7 @@ async def test_webhook_callback(
|
||||
signal_all.reset_mock()
|
||||
reolink_connect.get_motion_state_all_ch.return_value = False
|
||||
await client.post(f"/api/webhook/{webhook_id}")
|
||||
await hass.async_block_till_done()
|
||||
signal_all.assert_not_called()
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
@@ -137,6 +140,7 @@ async def test_webhook_callback(
|
||||
reolink_connect.motion_detected.return_value = False
|
||||
reolink_connect.ONVIF_event_callback.return_value = [0]
|
||||
await client.post(f"/api/webhook/{webhook_id}", data="test_data")
|
||||
await hass.async_block_till_done()
|
||||
signal_ch.assert_called_once()
|
||||
assert hass.states.get(entity_id).state == STATE_OFF
|
||||
|
||||
@@ -144,6 +148,7 @@ async def test_webhook_callback(
|
||||
signal_ch.reset_mock()
|
||||
reolink_connect.ONVIF_event_callback.side_effect = Exception("Test error")
|
||||
await client.post(f"/api/webhook/{webhook_id}", data="test_data")
|
||||
await hass.async_block_till_done()
|
||||
signal_ch.assert_not_called()
|
||||
|
||||
# test failure to read date from webhook post
|
||||
|
||||
@@ -69,7 +69,7 @@ from .conftest import (
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms")
|
||||
pytestmark = pytest.mark.usefixtures("reolink_host", "reolink_platforms")
|
||||
|
||||
CHIME_MODEL = "Reolink Chime"
|
||||
|
||||
@@ -116,15 +116,14 @@ async def test_wait(*args, **key_args) -> None:
|
||||
)
|
||||
async def test_failures_parametrized(
|
||||
hass: HomeAssistant,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
attr: str,
|
||||
value: Any,
|
||||
expected: ConfigEntryState,
|
||||
) -> None:
|
||||
"""Test outcomes when changing errors."""
|
||||
original = getattr(reolink_connect, attr)
|
||||
setattr(reolink_connect, attr, value)
|
||||
setattr(reolink_host, attr, value)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id) is (
|
||||
expected is ConfigEntryState.LOADED
|
||||
)
|
||||
@@ -132,17 +131,15 @@ async def test_failures_parametrized(
|
||||
|
||||
assert config_entry.state == expected
|
||||
|
||||
setattr(reolink_connect, attr, original)
|
||||
|
||||
|
||||
async def test_firmware_error_twice(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test when the firmware update fails 2 times."""
|
||||
reolink_connect.check_new_firmware.side_effect = ReolinkError("Test error")
|
||||
reolink_host.check_new_firmware.side_effect = ReolinkError("Test error")
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.UPDATE]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -158,13 +155,11 @@ async def test_firmware_error_twice(
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
|
||||
|
||||
reolink_connect.check_new_firmware.reset_mock(side_effect=True)
|
||||
|
||||
|
||||
async def test_credential_error_three(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
@@ -174,7 +169,7 @@ async def test_credential_error_three(
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
reolink_connect.get_states.side_effect = CredentialsInvalidError("Test error")
|
||||
reolink_host.get_states.side_effect = CredentialsInvalidError("Test error")
|
||||
|
||||
issue_id = f"config_entry_reauth_{DOMAIN}_{config_entry.entry_id}"
|
||||
for _ in range(NUM_CRED_ERRORS):
|
||||
@@ -185,31 +180,26 @@ async def test_credential_error_three(
|
||||
|
||||
assert (HOMEASSISTANT_DOMAIN, issue_id) in issue_registry.issues
|
||||
|
||||
reolink_connect.get_states.reset_mock(side_effect=True)
|
||||
|
||||
|
||||
async def test_entry_reloading(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
) -> None:
|
||||
"""Test the entry is reloaded correctly when settings change."""
|
||||
reolink_connect.is_nvr = False
|
||||
reolink_connect.logout.reset_mock()
|
||||
reolink_host.is_nvr = False
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert reolink_connect.logout.call_count == 0
|
||||
assert reolink_host.logout.call_count == 0
|
||||
assert config_entry.title == "test_reolink_name"
|
||||
|
||||
hass.config_entries.async_update_entry(config_entry, title="New Name")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert reolink_connect.logout.call_count == 1
|
||||
assert reolink_host.logout.call_count == 1
|
||||
assert config_entry.title == "New Name"
|
||||
|
||||
reolink_connect.is_nvr = True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("attr", "value", "expected_models"),
|
||||
@@ -241,7 +231,7 @@ async def test_removing_disconnected_cams(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
attr: str | None,
|
||||
@@ -249,7 +239,7 @@ async def test_removing_disconnected_cams(
|
||||
expected_models: list[str],
|
||||
) -> None:
|
||||
"""Test device and entity registry are cleaned up when camera is removed."""
|
||||
reolink_connect.channels = [0]
|
||||
reolink_host.channels = [0]
|
||||
assert await async_setup_component(hass, "config", {})
|
||||
client = await hass_ws_client(hass)
|
||||
# setup CH 0 and NVR switch entities/device
|
||||
@@ -265,8 +255,7 @@ async def test_removing_disconnected_cams(
|
||||
|
||||
# Try to remove the device after 'disconnecting' a camera.
|
||||
if attr is not None:
|
||||
original = getattr(reolink_connect, attr)
|
||||
setattr(reolink_connect, attr, value)
|
||||
setattr(reolink_host, attr, value)
|
||||
expected_success = TEST_CAM_MODEL not in expected_models
|
||||
for device in device_entries:
|
||||
if device.model == TEST_CAM_MODEL:
|
||||
@@ -279,9 +268,6 @@ async def test_removing_disconnected_cams(
|
||||
device_models = [device.model for device in device_entries]
|
||||
assert sorted(device_models) == sorted(expected_models)
|
||||
|
||||
if attr is not None:
|
||||
setattr(reolink_connect, attr, original)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("attr", "value", "expected_models"),
|
||||
@@ -307,8 +293,8 @@ async def test_removing_chime(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
test_chime: Chime,
|
||||
reolink_host: MagicMock,
|
||||
reolink_chime: Chime,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
attr: str | None,
|
||||
@@ -316,7 +302,7 @@ async def test_removing_chime(
|
||||
expected_models: list[str],
|
||||
) -> None:
|
||||
"""Test removing a chime."""
|
||||
reolink_connect.channels = [0]
|
||||
reolink_host.channels = [0]
|
||||
assert await async_setup_component(hass, "config", {})
|
||||
client = await hass_ws_client(hass)
|
||||
# setup CH 0 and NVR switch entities/device
|
||||
@@ -336,11 +322,11 @@ async def test_removing_chime(
|
||||
|
||||
async def test_remove_chime(*args, **key_args):
|
||||
"""Remove chime."""
|
||||
test_chime.connect_state = -1
|
||||
reolink_chime.connect_state = -1
|
||||
|
||||
test_chime.remove = test_remove_chime
|
||||
reolink_chime.remove = test_remove_chime
|
||||
elif attr is not None:
|
||||
setattr(test_chime, attr, value)
|
||||
setattr(reolink_chime, attr, value)
|
||||
|
||||
# Try to remove the device after 'disconnecting' a chime.
|
||||
expected_success = CHIME_MODEL not in expected_models
|
||||
@@ -444,7 +430,7 @@ async def test_removing_chime(
|
||||
async def test_migrate_entity_ids(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
original_id: str,
|
||||
@@ -464,8 +450,8 @@ async def test_migrate_entity_ids(
|
||||
return support_ch_uid
|
||||
return True
|
||||
|
||||
reolink_connect.channels = [0]
|
||||
reolink_connect.supported = mock_supported
|
||||
reolink_host.channels = [0]
|
||||
reolink_host.supported = mock_supported
|
||||
|
||||
dev_entry = device_registry.async_get_or_create(
|
||||
identifiers={(DOMAIN, original_dev_id)},
|
||||
@@ -513,7 +499,7 @@ async def test_migrate_entity_ids(
|
||||
async def test_migrate_with_already_existing_device(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
@@ -529,8 +515,8 @@ async def test_migrate_with_already_existing_device(
|
||||
return True
|
||||
return True
|
||||
|
||||
reolink_connect.channels = [0]
|
||||
reolink_connect.supported = mock_supported
|
||||
reolink_host.channels = [0]
|
||||
reolink_host.supported = mock_supported
|
||||
|
||||
device_registry.async_get_or_create(
|
||||
identifiers={(DOMAIN, new_dev_id)},
|
||||
@@ -562,7 +548,7 @@ async def test_migrate_with_already_existing_device(
|
||||
async def test_migrate_with_already_existing_entity(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
@@ -579,8 +565,8 @@ async def test_migrate_with_already_existing_entity(
|
||||
return True
|
||||
return True
|
||||
|
||||
reolink_connect.channels = [0]
|
||||
reolink_connect.supported = mock_supported
|
||||
reolink_host.channels = [0]
|
||||
reolink_host.supported = mock_supported
|
||||
|
||||
dev_entry = device_registry.async_get_or_create(
|
||||
identifiers={(DOMAIN, dev_id)},
|
||||
@@ -623,13 +609,13 @@ async def test_migrate_with_already_existing_entity(
|
||||
async def test_cleanup_mac_connection(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test cleanup of the MAC of a IPC which was set to the MAC of the host."""
|
||||
reolink_connect.channels = [0]
|
||||
reolink_connect.baichuan.mac_address.return_value = None
|
||||
reolink_host.channels = [0]
|
||||
reolink_host.baichuan.mac_address.return_value = None
|
||||
entity_id = f"{TEST_UID}_{TEST_UID_CAM}_record_audio"
|
||||
dev_id = f"{TEST_UID}_{TEST_UID_CAM}"
|
||||
domain = Platform.SWITCH
|
||||
@@ -666,19 +652,17 @@ async def test_cleanup_mac_connection(
|
||||
assert device
|
||||
assert device.connections == set()
|
||||
|
||||
reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM
|
||||
|
||||
|
||||
async def test_cleanup_combined_with_NVR(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test cleanup of the device registry if IPC camera device was combined with the NVR device."""
|
||||
reolink_connect.channels = [0]
|
||||
reolink_connect.baichuan.mac_address.return_value = None
|
||||
reolink_host.channels = [0]
|
||||
reolink_host.baichuan.mac_address.return_value = None
|
||||
entity_id = f"{TEST_UID}_{TEST_UID_CAM}_record_audio"
|
||||
dev_id = f"{TEST_UID}_{TEST_UID_CAM}"
|
||||
domain = Platform.SWITCH
|
||||
@@ -726,18 +710,16 @@ async def test_cleanup_combined_with_NVR(
|
||||
("OTHER_INTEGRATION", "SOME_ID"),
|
||||
}
|
||||
|
||||
reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM
|
||||
|
||||
|
||||
async def test_cleanup_hub_and_direct_connection(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test cleanup of the device registry if IPC camera device was connected directly and through the hub/NVR."""
|
||||
reolink_connect.channels = [0]
|
||||
reolink_host.channels = [0]
|
||||
entity_id = f"{TEST_UID}_{TEST_UID_CAM}_record_audio"
|
||||
dev_id = f"{TEST_UID}_{TEST_UID_CAM}"
|
||||
domain = Platform.SWITCH
|
||||
@@ -801,11 +783,11 @@ async def test_no_repair_issue(
|
||||
async def test_https_repair_issue(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test repairs issue is raised when https local url is used."""
|
||||
reolink_connect.get_states = test_wait
|
||||
reolink_host.get_states = test_wait
|
||||
await async_process_ha_core_config(
|
||||
hass, {"country": "GB", "internal_url": "https://test_homeassistant_address"}
|
||||
)
|
||||
@@ -828,11 +810,11 @@ async def test_https_repair_issue(
|
||||
async def test_ssl_repair_issue(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test repairs issue is raised when global ssl certificate is used."""
|
||||
reolink_connect.get_states = test_wait
|
||||
reolink_host.get_states = test_wait
|
||||
assert await async_setup_component(hass, "webhook", {})
|
||||
hass.config.api.use_ssl = True
|
||||
|
||||
@@ -859,32 +841,30 @@ async def test_ssl_repair_issue(
|
||||
async def test_port_repair_issue(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
protocol: str,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test repairs issue is raised when auto enable of ports fails."""
|
||||
reolink_connect.set_net_port.side_effect = ReolinkError("Test error")
|
||||
reolink_connect.onvif_enabled = False
|
||||
reolink_connect.rtsp_enabled = False
|
||||
reolink_connect.rtmp_enabled = False
|
||||
reolink_connect.protocol = protocol
|
||||
reolink_host.set_net_port.side_effect = ReolinkError("Test error")
|
||||
reolink_host.onvif_enabled = False
|
||||
reolink_host.rtsp_enabled = False
|
||||
reolink_host.rtmp_enabled = False
|
||||
reolink_host.protocol = protocol
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (DOMAIN, "enable_port") in issue_registry.issues
|
||||
|
||||
reolink_connect.set_net_port.reset_mock(side_effect=True)
|
||||
|
||||
|
||||
async def test_webhook_repair_issue(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test repairs issue is raised when the webhook url is unreachable."""
|
||||
reolink_connect.get_states = test_wait
|
||||
reolink_host.get_states = test_wait
|
||||
with (
|
||||
patch("homeassistant.components.reolink.host.FIRST_ONVIF_TIMEOUT", new=0),
|
||||
patch(
|
||||
@@ -903,25 +883,24 @@ async def test_webhook_repair_issue(
|
||||
async def test_firmware_repair_issue(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test firmware issue is raised when too old firmware is used."""
|
||||
reolink_connect.camera_sw_version_update_required.return_value = True
|
||||
reolink_host.camera_sw_version_update_required.return_value = True
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (DOMAIN, "firmware_update_host") in issue_registry.issues
|
||||
reolink_connect.camera_sw_version_update_required.return_value = False
|
||||
|
||||
|
||||
async def test_password_too_long_repair_issue(
|
||||
hass: HomeAssistant,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test password too long issue is raised."""
|
||||
reolink_connect.valid_password.return_value = False
|
||||
reolink_host.valid_password.return_value = False
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=format_mac(TEST_MAC),
|
||||
@@ -946,13 +925,12 @@ async def test_password_too_long_repair_issue(
|
||||
DOMAIN,
|
||||
f"password_too_long_{config_entry.entry_id}",
|
||||
) in issue_registry.issues
|
||||
reolink_connect.valid_password.return_value = True
|
||||
|
||||
|
||||
async def test_new_device_discovered(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the entry is reloaded when a new camera or chime is detected."""
|
||||
@@ -960,26 +938,24 @@ async def test_new_device_discovered(
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
reolink_connect.logout.reset_mock()
|
||||
|
||||
assert reolink_connect.logout.call_count == 0
|
||||
reolink_connect.new_devices = True
|
||||
assert reolink_host.logout.call_count == 0
|
||||
reolink_host.new_devices = True
|
||||
|
||||
freezer.tick(DEVICE_UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert reolink_connect.logout.call_count == 1
|
||||
assert reolink_host.logout.call_count == 1
|
||||
|
||||
|
||||
async def test_port_changed(
|
||||
hass: HomeAssistant,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test config_entry port update when it has changed during initial login."""
|
||||
assert config_entry.data[CONF_PORT] == TEST_PORT
|
||||
reolink_connect.port = 4567
|
||||
reolink_host.port = 4567
|
||||
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -989,12 +965,12 @@ async def test_port_changed(
|
||||
|
||||
async def test_baichuan_port_changed(
|
||||
hass: HomeAssistant,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test config_entry baichuan port update when it has changed during initial login."""
|
||||
assert config_entry.data[CONF_BC_PORT] == TEST_BC_PORT
|
||||
reolink_connect.baichuan.port = 8901
|
||||
reolink_host.baichuan.port = 8901
|
||||
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -1005,14 +981,12 @@ async def test_baichuan_port_changed(
|
||||
async def test_privacy_mode_on(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test successful setup even when privacy mode is turned on."""
|
||||
reolink_connect.baichuan.privacy_mode.return_value = True
|
||||
reolink_connect.get_states = AsyncMock(
|
||||
side_effect=LoginPrivacyModeError("Test error")
|
||||
)
|
||||
reolink_host.baichuan.privacy_mode.return_value = True
|
||||
reolink_host.get_states = AsyncMock(side_effect=LoginPrivacyModeError("Test error"))
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
@@ -1020,40 +994,36 @@ async def test_privacy_mode_on(
|
||||
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
reolink_connect.baichuan.privacy_mode.return_value = False
|
||||
|
||||
|
||||
async def test_LoginPrivacyModeError(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test normal update when get_states returns a LoginPrivacyModeError."""
|
||||
reolink_connect.baichuan.privacy_mode.return_value = False
|
||||
reolink_connect.get_states = AsyncMock(
|
||||
side_effect=LoginPrivacyModeError("Test error")
|
||||
)
|
||||
reolink_host.baichuan.privacy_mode.return_value = False
|
||||
reolink_host.get_states = AsyncMock(side_effect=LoginPrivacyModeError("Test error"))
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
reolink_connect.baichuan.check_subscribe_events.reset_mock()
|
||||
assert reolink_connect.baichuan.check_subscribe_events.call_count == 0
|
||||
reolink_host.baichuan.check_subscribe_events.reset_mock()
|
||||
assert reolink_host.baichuan.check_subscribe_events.call_count == 0
|
||||
|
||||
freezer.tick(DEVICE_UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert reolink_connect.baichuan.check_subscribe_events.call_count >= 1
|
||||
assert reolink_host.baichuan.check_subscribe_events.call_count >= 1
|
||||
|
||||
|
||||
async def test_privacy_mode_change_callback(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
) -> None:
|
||||
"""Test privacy mode changed callback."""
|
||||
|
||||
@@ -1068,13 +1038,12 @@ async def test_privacy_mode_change_callback(
|
||||
|
||||
callback_mock = callback_mock_class()
|
||||
|
||||
reolink_connect.model = TEST_HOST_MODEL
|
||||
reolink_connect.baichuan.events_active = True
|
||||
reolink_connect.baichuan.subscribe_events.reset_mock(side_effect=True)
|
||||
reolink_connect.baichuan.register_callback = callback_mock.register_callback
|
||||
reolink_connect.baichuan.privacy_mode.return_value = True
|
||||
reolink_connect.audio_record.return_value = True
|
||||
reolink_connect.get_states = AsyncMock()
|
||||
reolink_host.model = TEST_HOST_MODEL
|
||||
reolink_host.baichuan.events_active = True
|
||||
reolink_host.baichuan.subscribe_events.reset_mock(side_effect=True)
|
||||
reolink_host.baichuan.register_callback = callback_mock.register_callback
|
||||
reolink_host.baichuan.privacy_mode.return_value = True
|
||||
reolink_host.audio_record.return_value = True
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
@@ -1085,29 +1054,29 @@ async def test_privacy_mode_change_callback(
|
||||
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
|
||||
|
||||
# simulate a TCP push callback signaling a privacy mode change
|
||||
reolink_connect.baichuan.privacy_mode.return_value = False
|
||||
reolink_host.baichuan.privacy_mode.return_value = False
|
||||
assert callback_mock.callback_func is not None
|
||||
callback_mock.callback_func()
|
||||
|
||||
# check that a coordinator update was scheduled.
|
||||
reolink_connect.get_states.reset_mock()
|
||||
assert reolink_connect.get_states.call_count == 0
|
||||
reolink_host.get_states.reset_mock()
|
||||
assert reolink_host.get_states.call_count == 0
|
||||
|
||||
freezer.tick(5)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert reolink_connect.get_states.call_count >= 1
|
||||
assert reolink_host.get_states.call_count >= 1
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
|
||||
# test cleanup during unloading, first reset to privacy mode ON
|
||||
reolink_connect.baichuan.privacy_mode.return_value = True
|
||||
reolink_host.baichuan.privacy_mode.return_value = True
|
||||
callback_mock.callback_func()
|
||||
freezer.tick(5)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
# now fire the callback again, but unload before refresh took place
|
||||
reolink_connect.baichuan.privacy_mode.return_value = False
|
||||
reolink_host.baichuan.privacy_mode.return_value = False
|
||||
callback_mock.callback_func()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -1120,7 +1089,7 @@ async def test_camera_wake_callback(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
) -> None:
|
||||
"""Test camera wake callback."""
|
||||
|
||||
@@ -1135,13 +1104,12 @@ async def test_camera_wake_callback(
|
||||
|
||||
callback_mock = callback_mock_class()
|
||||
|
||||
reolink_connect.model = TEST_HOST_MODEL
|
||||
reolink_connect.baichuan.events_active = True
|
||||
reolink_connect.baichuan.subscribe_events.reset_mock(side_effect=True)
|
||||
reolink_connect.baichuan.register_callback = callback_mock.register_callback
|
||||
reolink_connect.sleeping.return_value = True
|
||||
reolink_connect.audio_record.return_value = True
|
||||
reolink_connect.get_states = AsyncMock()
|
||||
reolink_host.model = TEST_HOST_MODEL
|
||||
reolink_host.baichuan.events_active = True
|
||||
reolink_host.baichuan.subscribe_events.reset_mock(side_effect=True)
|
||||
reolink_host.baichuan.register_callback = callback_mock.register_callback
|
||||
reolink_host.sleeping.return_value = True
|
||||
reolink_host.audio_record.return_value = True
|
||||
|
||||
with (
|
||||
patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]),
|
||||
@@ -1157,12 +1125,12 @@ async def test_camera_wake_callback(
|
||||
entity_id = f"{Platform.SWITCH}.{TEST_NVR_NAME}_record_audio"
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
|
||||
reolink_connect.sleeping.return_value = False
|
||||
reolink_connect.get_states.reset_mock()
|
||||
assert reolink_connect.get_states.call_count == 0
|
||||
reolink_host.sleeping.return_value = False
|
||||
reolink_host.get_states.reset_mock()
|
||||
assert reolink_host.get_states.call_count == 0
|
||||
|
||||
# simulate a TCP push callback signaling the battery camera woke up
|
||||
reolink_connect.audio_record.return_value = False
|
||||
reolink_host.audio_record.return_value = False
|
||||
assert callback_mock.callback_func is not None
|
||||
with (
|
||||
patch(
|
||||
@@ -1182,13 +1150,26 @@ async def test_camera_wake_callback(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# check that a coordinator update was scheduled.
|
||||
assert reolink_connect.get_states.call_count >= 1
|
||||
assert reolink_host.get_states.call_count >= 1
|
||||
assert hass.states.get(entity_id).state == STATE_OFF
|
||||
|
||||
|
||||
async def test_baichaun_only(
|
||||
hass: HomeAssistant,
|
||||
reolink_connect: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test initializing a baichuan only device."""
|
||||
reolink_connect.baichuan_only = True
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_remove(
|
||||
hass: HomeAssistant,
|
||||
reolink_connect: MagicMock,
|
||||
reolink_host: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test removing of the reolink integration."""
|
||||
|
||||
@@ -53,6 +53,6 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1',
|
||||
'state': '1.0',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -93,11 +93,11 @@ async def test_number_state_update(
|
||||
|
||||
entity_id = entity_info["entity_id"]
|
||||
|
||||
assert hass.states.get(entity_id).state == "1"
|
||||
assert hass.states.get(entity_id).state == "1.0"
|
||||
|
||||
mock_number_property.get.return_value = 100
|
||||
|
||||
await update_property_listeners(mock_number_property)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(entity_id).state == "100"
|
||||
assert hass.states.get(entity_id).state == "100.0"
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_000001][switch.refrigerator_ice_cubes-entry]
|
||||
# name: test_all_entities[da_ref_normal_000001][switch.refrigerator_cubed_ice-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -60,7 +60,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.refrigerator_ice_cubes',
|
||||
'entity_id': 'switch.refrigerator_cubed_ice',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -72,7 +72,7 @@
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Ice cubes',
|
||||
'original_name': 'Cubed ice',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
@@ -82,13 +82,13 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_000001][switch.refrigerator_ice_cubes-state]
|
||||
# name: test_all_entities[da_ref_normal_000001][switch.refrigerator_cubed_ice-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Refrigerator Ice cubes',
|
||||
'friendly_name': 'Refrigerator Cubed ice',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.refrigerator_ice_cubes',
|
||||
'entity_id': 'switch.refrigerator_cubed_ice',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
@@ -239,7 +239,7 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][switch.refrigerator_ice_cubes-entry]
|
||||
# name: test_all_entities[da_ref_normal_01001][switch.refrigerator_cubed_ice-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -252,7 +252,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.refrigerator_ice_cubes',
|
||||
'entity_id': 'switch.refrigerator_cubed_ice',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -264,7 +264,7 @@
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Ice cubes',
|
||||
'original_name': 'Cubed ice',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
@@ -274,13 +274,13 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01001][switch.refrigerator_ice_cubes-state]
|
||||
# name: test_all_entities[da_ref_normal_01001][switch.refrigerator_cubed_ice-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Refrigerator Ice cubes',
|
||||
'friendly_name': 'Refrigerator Cubed ice',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.refrigerator_ice_cubes',
|
||||
'entity_id': 'switch.refrigerator_cubed_ice',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
@@ -383,6 +383,54 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01011][switch.frigo_cubed_ice-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.frigo_cubed_ice',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cubed ice',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'ice_maker',
|
||||
'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_icemaker_switch_switch_switch',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01011][switch.frigo_cubed_ice-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Frigo Cubed ice',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.frigo_cubed_ice',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01011][switch.frigo_ice_bites-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -408,7 +456,7 @@
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Ice bites',
|
||||
'original_name': 'Ice Bites',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
@@ -421,7 +469,7 @@
|
||||
# name: test_all_entities[da_ref_normal_01011][switch.frigo_ice_bites-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Frigo Ice bites',
|
||||
'friendly_name': 'Frigo Ice Bites',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.frigo_ice_bites',
|
||||
@@ -431,54 +479,6 @@
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01011][switch.frigo_ice_cubes-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.frigo_ice_cubes',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Ice cubes',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'ice_maker',
|
||||
'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_icemaker_switch_switch_switch',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01011][switch.frigo_ice_cubes-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Frigo Ice cubes',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.frigo_ice_cubes',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ref_normal_01011][switch.frigo_power_cool-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -85,6 +85,15 @@ class SonosMockService:
|
||||
self.subscribe = AsyncMock(return_value=SonosMockSubscribe(ip_address))
|
||||
|
||||
|
||||
class SonosMockRenderingService(SonosMockService):
|
||||
"""Mock rendering service."""
|
||||
|
||||
def __init__(self, return_value: dict[str, str], ip_address="192.168.42.2") -> None:
|
||||
"""Initialize the instance."""
|
||||
super().__init__("RenderingControl", ip_address)
|
||||
self.GetVolume = Mock(return_value=30)
|
||||
|
||||
|
||||
class SonosMockAlarmClock(SonosMockService):
|
||||
"""Mock a Sonos AlarmClock Service used in callbacks."""
|
||||
|
||||
@@ -239,7 +248,7 @@ class SoCoMockFactory:
|
||||
mock_soco.avTransport.GetPositionInfo = Mock(
|
||||
return_value=self.current_track_info
|
||||
)
|
||||
mock_soco.renderingControl = SonosMockService("RenderingControl", ip_address)
|
||||
mock_soco.renderingControl = SonosMockRenderingService(ip_address)
|
||||
mock_soco.zoneGroupTopology = SonosMockService("ZoneGroupTopology", ip_address)
|
||||
mock_soco.contentDirectory = SonosMockService("ContentDirectory", ip_address)
|
||||
mock_soco.deviceProperties = SonosMockService("DeviceProperties", ip_address)
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import Mock, PropertyMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import sonos
|
||||
from homeassistant.components.sonos import SonosDiscoveryManager
|
||||
from homeassistant.components.sonos.const import (
|
||||
DATA_SONOS_DISCOVERY_MANAGER,
|
||||
DISCOVERY_INTERVAL,
|
||||
SONOS_SPEAKER_ACTIVITY,
|
||||
)
|
||||
from homeassistant.components.sonos.exception import SonosUpdateError
|
||||
@@ -87,76 +87,73 @@ async def test_not_configuring_sonos_not_creates_entry(hass: HomeAssistant) -> N
|
||||
|
||||
|
||||
async def test_async_poll_manual_hosts_warnings(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
soco_factory: SoCoMockFactory,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test that host warnings are not logged repeatedly."""
|
||||
await async_setup_component(
|
||||
hass,
|
||||
sonos.DOMAIN,
|
||||
{"sonos": {"media_player": {"interface_addr": "127.0.0.1"}}},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
manager: SonosDiscoveryManager = hass.data[DATA_SONOS_DISCOVERY_MANAGER]
|
||||
manager.hosts.add("10.10.10.10")
|
||||
|
||||
soco = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Bedroom")
|
||||
with (
|
||||
caplog.at_level(logging.DEBUG),
|
||||
patch.object(manager, "_async_handle_discovery_message"),
|
||||
patch(
|
||||
"homeassistant.components.sonos.async_call_later"
|
||||
) as mock_async_call_later,
|
||||
patch("homeassistant.components.sonos.async_dispatcher_send"),
|
||||
patch(
|
||||
"homeassistant.components.sonos.sync_get_visible_zones",
|
||||
side_effect=[
|
||||
OSError(),
|
||||
OSError(),
|
||||
[],
|
||||
[],
|
||||
OSError(),
|
||||
],
|
||||
),
|
||||
patch.object(
|
||||
type(soco), "visible_zones", new_callable=PropertyMock
|
||||
) as mock_visible_zones,
|
||||
):
|
||||
# First call fails, it should be logged as a WARNING message
|
||||
mock_visible_zones.side_effect = OSError()
|
||||
caplog.clear()
|
||||
await manager.async_poll_manual_hosts()
|
||||
assert len(caplog.messages) == 1
|
||||
record = caplog.records[0]
|
||||
assert record.levelname == "WARNING"
|
||||
assert "Could not get visible Sonos devices from" in record.message
|
||||
assert mock_async_call_later.call_count == 1
|
||||
await _setup_hass(hass)
|
||||
assert [
|
||||
rec.levelname
|
||||
for rec in caplog.records
|
||||
if "Could not get visible Sonos devices from" in rec.message
|
||||
] == ["WARNING"]
|
||||
|
||||
# Second call fails again, it should be logged as a DEBUG message
|
||||
mock_visible_zones.side_effect = OSError()
|
||||
caplog.clear()
|
||||
await manager.async_poll_manual_hosts()
|
||||
assert len(caplog.messages) == 1
|
||||
record = caplog.records[0]
|
||||
assert record.levelname == "DEBUG"
|
||||
assert "Could not get visible Sonos devices from" in record.message
|
||||
assert mock_async_call_later.call_count == 2
|
||||
freezer.tick(DISCOVERY_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert [
|
||||
rec.levelname
|
||||
for rec in caplog.records
|
||||
if "Could not get visible Sonos devices from" in rec.message
|
||||
] == ["DEBUG"]
|
||||
|
||||
# Third call succeeds, it should log an info message
|
||||
# Third call succeeds, logs message indicating reconnect
|
||||
mock_visible_zones.return_value = {soco}
|
||||
mock_visible_zones.side_effect = None
|
||||
caplog.clear()
|
||||
await manager.async_poll_manual_hosts()
|
||||
assert len(caplog.messages) == 1
|
||||
record = caplog.records[0]
|
||||
assert record.levelname == "WARNING"
|
||||
assert "Connection reestablished to Sonos device" in record.message
|
||||
assert mock_async_call_later.call_count == 3
|
||||
freezer.tick(DISCOVERY_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert [
|
||||
rec.levelname
|
||||
for rec in caplog.records
|
||||
if "Connection reestablished to Sonos device" in rec.message
|
||||
] == ["WARNING"]
|
||||
|
||||
# Fourth call succeeds again, no need to log
|
||||
# Fourth call succeeds, it should log nothing
|
||||
caplog.clear()
|
||||
await manager.async_poll_manual_hosts()
|
||||
assert len(caplog.messages) == 0
|
||||
assert mock_async_call_later.call_count == 4
|
||||
freezer.tick(DISCOVERY_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert "Connection reestablished to Sonos device" not in caplog.text
|
||||
|
||||
# Fifth call fail again again, should be logged as a WARNING message
|
||||
# Fifth call fails again again, should be logged as a WARNING message
|
||||
mock_visible_zones.side_effect = OSError()
|
||||
caplog.clear()
|
||||
await manager.async_poll_manual_hosts()
|
||||
assert len(caplog.messages) == 1
|
||||
record = caplog.records[0]
|
||||
assert record.levelname == "WARNING"
|
||||
assert "Could not get visible Sonos devices from" in record.message
|
||||
assert mock_async_call_later.call_count == 5
|
||||
freezer.tick(DISCOVERY_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert [
|
||||
rec.levelname
|
||||
for rec in caplog.records
|
||||
if "Could not get visible Sonos devices from" in rec.message
|
||||
] == ["WARNING"]
|
||||
|
||||
|
||||
class _MockSoCoOsError(MockSoCo):
|
||||
|
||||
@@ -16,6 +16,22 @@ from homeassistant.components.blueprint import (
|
||||
DomainBlueprints,
|
||||
)
|
||||
from homeassistant.components.template import DOMAIN, SERVICE_RELOAD
|
||||
from homeassistant.components.template.config import (
|
||||
DOMAIN_ALARM_CONTROL_PANEL,
|
||||
DOMAIN_BINARY_SENSOR,
|
||||
DOMAIN_COVER,
|
||||
DOMAIN_FAN,
|
||||
DOMAIN_IMAGE,
|
||||
DOMAIN_LIGHT,
|
||||
DOMAIN_LOCK,
|
||||
DOMAIN_NUMBER,
|
||||
DOMAIN_SELECT,
|
||||
DOMAIN_SENSOR,
|
||||
DOMAIN_SWITCH,
|
||||
DOMAIN_VACUUM,
|
||||
DOMAIN_WEATHER,
|
||||
)
|
||||
from homeassistant.const import STATE_ON
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -459,3 +475,51 @@ async def test_no_blueprint(hass: HomeAssistant) -> None:
|
||||
template.helpers.blueprint_in_template(hass, "binary_sensor.test_entity")
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("domain", "set_state", "expected"),
|
||||
[
|
||||
(DOMAIN_ALARM_CONTROL_PANEL, STATE_ON, "armed_home"),
|
||||
(DOMAIN_BINARY_SENSOR, STATE_ON, STATE_ON),
|
||||
(DOMAIN_COVER, STATE_ON, "open"),
|
||||
(DOMAIN_FAN, STATE_ON, STATE_ON),
|
||||
(DOMAIN_IMAGE, "test.jpg", "2025-06-13T00:00:00+00:00"),
|
||||
(DOMAIN_LIGHT, STATE_ON, STATE_ON),
|
||||
(DOMAIN_LOCK, STATE_ON, "locked"),
|
||||
(DOMAIN_NUMBER, "1", "1.0"),
|
||||
(DOMAIN_SELECT, "option1", "option1"),
|
||||
(DOMAIN_SENSOR, "foo", "foo"),
|
||||
(DOMAIN_SWITCH, STATE_ON, STATE_ON),
|
||||
(DOMAIN_VACUUM, "cleaning", "cleaning"),
|
||||
(DOMAIN_WEATHER, "sunny", "sunny"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.freeze_time("2025-06-13 00:00:00+00:00")
|
||||
async def test_variables_for_entity(
|
||||
hass: HomeAssistant, domain: str, set_state: str, expected: str
|
||||
) -> None:
|
||||
"""Test regular template entities via blueprint with variables defined."""
|
||||
hass.states.async_set("sensor.test_state", set_state)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
"template",
|
||||
{
|
||||
"template": [
|
||||
{
|
||||
"use_blueprint": {
|
||||
"path": f"test_{domain}_with_variables.yaml",
|
||||
"input": {"sensor": "sensor.test_state"},
|
||||
},
|
||||
"name": "Test",
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(f"{domain}.test")
|
||||
assert state is not None
|
||||
assert state.state == expected
|
||||
|
||||
@@ -11,7 +11,10 @@ from homeassistant import setup
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||
from homeassistant.components.template import DOMAIN
|
||||
from homeassistant.components.template.button import DEFAULT_NAME
|
||||
from homeassistant.components.template.const import CONF_PICTURE
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_PICTURE,
|
||||
ATTR_ICON,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_FRIENDLY_NAME,
|
||||
@@ -247,6 +250,49 @@ async def test_name_template(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("field", "attribute", "test_template", "expected"),
|
||||
[
|
||||
(CONF_ICON, ATTR_ICON, "mdi:test{{ 1 + 1 }}", "mdi:test2"),
|
||||
(CONF_PICTURE, ATTR_ENTITY_PICTURE, "test{{ 1 + 1 }}.jpg", "test2.jpg"),
|
||||
],
|
||||
)
|
||||
async def test_templated_optional_config(
|
||||
hass: HomeAssistant,
|
||||
field: str,
|
||||
attribute: str,
|
||||
test_template: str,
|
||||
expected: str,
|
||||
) -> None:
|
||||
"""Test optional config templates."""
|
||||
with assert_setup_component(1, "template"):
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
"template",
|
||||
{
|
||||
"template": {
|
||||
"button": {
|
||||
"press": {"service": "script.press"},
|
||||
field: test_template,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
_verify(
|
||||
hass,
|
||||
STATE_UNKNOWN,
|
||||
{
|
||||
attribute: expected,
|
||||
},
|
||||
"button.template_button",
|
||||
)
|
||||
|
||||
|
||||
async def test_unique_id(hass: HomeAssistant) -> None:
|
||||
"""Test: unique id is ok."""
|
||||
with assert_setup_component(1, "template"):
|
||||
|
||||
@@ -21,10 +21,13 @@ from homeassistant.components.number import (
|
||||
SERVICE_SET_VALUE as NUMBER_SERVICE_SET_VALUE,
|
||||
)
|
||||
from homeassistant.components.template import DOMAIN
|
||||
from homeassistant.components.template.const import CONF_PICTURE
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_ENTITY_PICTURE,
|
||||
ATTR_ICON,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_ICON,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
@@ -58,6 +61,20 @@ _VALUE_INPUT_NUMBER_CONFIG = {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_STATE_ENTITY_ID = "number.test_state"
|
||||
|
||||
TEST_STATE_TRIGGER = {
|
||||
"trigger": {
|
||||
"trigger": "state",
|
||||
"entity_id": [TEST_STATE_ENTITY_ID],
|
||||
},
|
||||
"variables": {"triggering_entity": "{{ trigger.entity_id }}"},
|
||||
"action": [
|
||||
{"event": "action_event", "event_data": {"what": "{{ triggering_entity }}"}}
|
||||
],
|
||||
}
|
||||
TEST_REQUIRED = {"state": "0", "step": "1", "set_value": []}
|
||||
|
||||
|
||||
async def async_setup_modern_format(
|
||||
hass: HomeAssistant, count: int, number_config: dict[str, Any]
|
||||
@@ -77,6 +94,24 @@ async def async_setup_modern_format(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def async_setup_trigger_format(
|
||||
hass: HomeAssistant, count: int, number_config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Do setup of number integration via trigger format."""
|
||||
config = {"template": {**TEST_STATE_TRIGGER, "number": number_config}}
|
||||
|
||||
with assert_setup_component(count, template.DOMAIN):
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
template.DOMAIN,
|
||||
config,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_number(
|
||||
hass: HomeAssistant,
|
||||
@@ -89,6 +124,10 @@ async def setup_number(
|
||||
await async_setup_modern_format(
|
||||
hass, count, {"name": _TEST_OBJECT_ID, **number_config}
|
||||
)
|
||||
if style == ConfigurationStyle.TRIGGER:
|
||||
await async_setup_trigger_format(
|
||||
hass, count, {"name": _TEST_OBJECT_ID, **number_config}
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_config_entry(
|
||||
@@ -446,119 +485,49 @@ def _verify(
|
||||
assert attributes.get(CONF_UNIT_OF_MEASUREMENT) == expected_unit_of_measurement
|
||||
|
||||
|
||||
async def test_icon_template(hass: HomeAssistant) -> None:
|
||||
"""Test template numbers with icon templates."""
|
||||
with assert_setup_component(1, "input_number"):
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
"input_number",
|
||||
{"input_number": _VALUE_INPUT_NUMBER_CONFIG},
|
||||
)
|
||||
|
||||
with assert_setup_component(1, "template"):
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
"template",
|
||||
@pytest.mark.parametrize("count", [1])
|
||||
@pytest.mark.parametrize(
|
||||
("style", "initial_expected_state"),
|
||||
[(ConfigurationStyle.MODERN, ""), (ConfigurationStyle.TRIGGER, None)],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("number_config", "attribute", "expected"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"template": {
|
||||
"unique_id": "b",
|
||||
"number": {
|
||||
"state": f"{{{{ states('{_VALUE_INPUT_NUMBER}') }}}}",
|
||||
"step": 1,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"set_value": {
|
||||
"service": "input_number.set_value",
|
||||
"data_template": {
|
||||
"entity_id": _VALUE_INPUT_NUMBER,
|
||||
"value": "{{ value }}",
|
||||
},
|
||||
},
|
||||
"icon": "{% if ((states.input_number.value.state or 0) | int) > 50 %}mdi:greater{% else %}mdi:less{% endif %}",
|
||||
},
|
||||
}
|
||||
CONF_ICON: "{% if states.number.test_state.state == '1' %}mdi:check{% endif %}",
|
||||
**TEST_REQUIRED,
|
||||
},
|
||||
)
|
||||
|
||||
hass.states.async_set(_VALUE_INPUT_NUMBER, 49)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(_TEST_NUMBER)
|
||||
assert float(state.state) == 49
|
||||
assert state.attributes[ATTR_ICON] == "mdi:less"
|
||||
|
||||
await hass.services.async_call(
|
||||
INPUT_NUMBER_DOMAIN,
|
||||
INPUT_NUMBER_SERVICE_SET_VALUE,
|
||||
{CONF_ENTITY_ID: _VALUE_INPUT_NUMBER, INPUT_NUMBER_ATTR_VALUE: 51},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(_TEST_NUMBER)
|
||||
assert float(state.state) == 51
|
||||
assert state.attributes[ATTR_ICON] == "mdi:greater"
|
||||
|
||||
|
||||
async def test_icon_template_with_trigger(hass: HomeAssistant) -> None:
|
||||
"""Test template numbers with icon templates."""
|
||||
with assert_setup_component(1, "input_number"):
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
"input_number",
|
||||
{"input_number": _VALUE_INPUT_NUMBER_CONFIG},
|
||||
)
|
||||
|
||||
with assert_setup_component(1, "template"):
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
"template",
|
||||
ATTR_ICON,
|
||||
"mdi:check",
|
||||
),
|
||||
(
|
||||
{
|
||||
"template": {
|
||||
"trigger": {"platform": "state", "entity_id": _VALUE_INPUT_NUMBER},
|
||||
"unique_id": "b",
|
||||
"number": {
|
||||
"state": "{{ trigger.to_state.state }}",
|
||||
"step": 1,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"set_value": {
|
||||
"service": "input_number.set_value",
|
||||
"data_template": {
|
||||
"entity_id": _VALUE_INPUT_NUMBER,
|
||||
"value": "{{ value }}",
|
||||
},
|
||||
},
|
||||
"icon": "{% if ((trigger.to_state.state or 0) | int) > 50 %}mdi:greater{% else %}mdi:less{% endif %}",
|
||||
},
|
||||
}
|
||||
CONF_PICTURE: "{% if states.number.test_state.state == '1' %}check.jpg{% endif %}",
|
||||
**TEST_REQUIRED,
|
||||
},
|
||||
)
|
||||
ATTR_ENTITY_PICTURE,
|
||||
"check.jpg",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_number")
|
||||
async def test_templated_optional_config(
|
||||
hass: HomeAssistant,
|
||||
attribute: str,
|
||||
expected: str,
|
||||
initial_expected_state: str | None,
|
||||
) -> None:
|
||||
"""Test optional config templates."""
|
||||
state = hass.states.get(_TEST_NUMBER)
|
||||
assert state.attributes.get(attribute) == initial_expected_state
|
||||
|
||||
hass.states.async_set(_VALUE_INPUT_NUMBER, 49)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
state = hass.states.async_set(TEST_STATE_ENTITY_ID, "1")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(_TEST_NUMBER)
|
||||
assert float(state.state) == 49
|
||||
assert state.attributes[ATTR_ICON] == "mdi:less"
|
||||
|
||||
await hass.services.async_call(
|
||||
INPUT_NUMBER_DOMAIN,
|
||||
INPUT_NUMBER_SERVICE_SET_VALUE,
|
||||
{CONF_ENTITY_ID: _VALUE_INPUT_NUMBER, INPUT_NUMBER_ATTR_VALUE: 51},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(_TEST_NUMBER)
|
||||
assert float(state.state) == 51
|
||||
assert state.attributes[ATTR_ICON] == "mdi:greater"
|
||||
assert state.attributes[attribute] == expected
|
||||
|
||||
|
||||
async def test_device_id(
|
||||
|
||||
@@ -21,7 +21,15 @@ from homeassistant.components.select import (
|
||||
SERVICE_SELECT_OPTION as SELECT_SERVICE_SELECT_OPTION,
|
||||
)
|
||||
from homeassistant.components.template import DOMAIN
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, CONF_ENTITY_ID, STATE_UNKNOWN
|
||||
from homeassistant.components.template.const import CONF_PICTURE
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_ENTITY_PICTURE,
|
||||
ATTR_ICON,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_ICON,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import Context, HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -34,6 +42,24 @@ _TEST_OBJECT_ID = "template_select"
|
||||
_TEST_SELECT = f"select.{_TEST_OBJECT_ID}"
|
||||
# Represent for select's current_option
|
||||
_OPTION_INPUT_SELECT = "input_select.option"
|
||||
TEST_STATE_ENTITY_ID = "select.test_state"
|
||||
|
||||
TEST_STATE_TRIGGER = {
|
||||
"trigger": {
|
||||
"trigger": "state",
|
||||
"entity_id": [_OPTION_INPUT_SELECT, TEST_STATE_ENTITY_ID],
|
||||
},
|
||||
"variables": {"triggering_entity": "{{ trigger.entity_id }}"},
|
||||
"action": [
|
||||
{"event": "action_event", "event_data": {"what": "{{ triggering_entity }}"}}
|
||||
],
|
||||
}
|
||||
|
||||
TEST_OPTIONS = {
|
||||
"state": "test",
|
||||
"options": "{{ ['test', 'yes', 'no'] }}",
|
||||
"select_option": [],
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_modern_format(
|
||||
@@ -54,6 +80,24 @@ async def async_setup_modern_format(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def async_setup_trigger_format(
|
||||
hass: HomeAssistant, count: int, select_config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Do setup of select integration via trigger format."""
|
||||
config = {"template": {**TEST_STATE_TRIGGER, "select": select_config}}
|
||||
|
||||
with assert_setup_component(count, template.DOMAIN):
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
template.DOMAIN,
|
||||
config,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_select(
|
||||
hass: HomeAssistant,
|
||||
@@ -66,6 +110,10 @@ async def setup_select(
|
||||
await async_setup_modern_format(
|
||||
hass, count, {"name": _TEST_OBJECT_ID, **select_config}
|
||||
)
|
||||
if style == ConfigurationStyle.TRIGGER:
|
||||
await async_setup_trigger_format(
|
||||
hass, count, {"name": _TEST_OBJECT_ID, **select_config}
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_config_entry(
|
||||
@@ -395,138 +443,49 @@ def _verify(
|
||||
assert attributes.get(SELECT_ATTR_OPTIONS) == expected_options
|
||||
|
||||
|
||||
async def test_template_icon_with_entities(hass: HomeAssistant) -> None:
|
||||
"""Test templates with values from other entities."""
|
||||
with assert_setup_component(1, "input_select"):
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
"input_select",
|
||||
@pytest.mark.parametrize("count", [1])
|
||||
@pytest.mark.parametrize(
|
||||
("style", "initial_expected_state"),
|
||||
[(ConfigurationStyle.MODERN, ""), (ConfigurationStyle.TRIGGER, None)],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("select_config", "attribute", "expected"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"input_select": {
|
||||
"option": {
|
||||
"options": ["a", "b"],
|
||||
"initial": "a",
|
||||
"name": "Option",
|
||||
},
|
||||
}
|
||||
**TEST_OPTIONS,
|
||||
CONF_ICON: "{% if states.select.test_state.state == 'yes' %}mdi:check{% endif %}",
|
||||
},
|
||||
)
|
||||
|
||||
with assert_setup_component(1, "template"):
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
"template",
|
||||
ATTR_ICON,
|
||||
"mdi:check",
|
||||
),
|
||||
(
|
||||
{
|
||||
"template": {
|
||||
"unique_id": "b",
|
||||
"select": {
|
||||
"state": f"{{{{ states('{_OPTION_INPUT_SELECT}') }}}}",
|
||||
"options": f"{{{{ state_attr('{_OPTION_INPUT_SELECT}', '{INPUT_SELECT_ATTR_OPTIONS}') }}}}",
|
||||
"select_option": {
|
||||
"service": "input_select.select_option",
|
||||
"data": {
|
||||
"entity_id": _OPTION_INPUT_SELECT,
|
||||
"option": "{{ option }}",
|
||||
},
|
||||
},
|
||||
"optimistic": True,
|
||||
"unique_id": "a",
|
||||
"icon": f"{{% if (states('{_OPTION_INPUT_SELECT}') == 'a') %}}mdi:greater{{% else %}}mdi:less{{% endif %}}",
|
||||
},
|
||||
}
|
||||
**TEST_OPTIONS,
|
||||
CONF_PICTURE: "{% if states.select.test_state.state == 'yes' %}check.jpg{% endif %}",
|
||||
},
|
||||
)
|
||||
ATTR_ENTITY_PICTURE,
|
||||
"check.jpg",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_select")
|
||||
async def test_templated_optional_config(
|
||||
hass: HomeAssistant,
|
||||
attribute: str,
|
||||
expected: str,
|
||||
initial_expected_state: str | None,
|
||||
) -> None:
|
||||
"""Test optional config templates."""
|
||||
state = hass.states.get(_TEST_SELECT)
|
||||
assert state.attributes.get(attribute) == initial_expected_state
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
state = hass.states.async_set(TEST_STATE_ENTITY_ID, "yes")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(_TEST_SELECT)
|
||||
assert state.state == "a"
|
||||
assert state.attributes[ATTR_ICON] == "mdi:greater"
|
||||
|
||||
await hass.services.async_call(
|
||||
INPUT_SELECT_DOMAIN,
|
||||
INPUT_SELECT_SERVICE_SELECT_OPTION,
|
||||
{CONF_ENTITY_ID: _OPTION_INPUT_SELECT, INPUT_SELECT_ATTR_OPTION: "b"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(_TEST_SELECT)
|
||||
assert state.state == "b"
|
||||
assert state.attributes[ATTR_ICON] == "mdi:less"
|
||||
|
||||
|
||||
async def test_template_icon_with_trigger(hass: HomeAssistant) -> None:
|
||||
"""Test trigger based template select."""
|
||||
with assert_setup_component(1, "input_select"):
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
"input_select",
|
||||
{
|
||||
"input_select": {
|
||||
"option": {
|
||||
"options": ["a", "b"],
|
||||
"initial": "a",
|
||||
"name": "Option",
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
"template",
|
||||
{
|
||||
"template": {
|
||||
"trigger": {"platform": "state", "entity_id": _OPTION_INPUT_SELECT},
|
||||
"select": {
|
||||
"unique_id": "b",
|
||||
"state": "{{ trigger.to_state.state }}",
|
||||
"options": f"{{{{ state_attr('{_OPTION_INPUT_SELECT}', '{INPUT_SELECT_ATTR_OPTIONS}') }}}}",
|
||||
"select_option": {
|
||||
"service": "input_select.select_option",
|
||||
"data": {
|
||||
"entity_id": _OPTION_INPUT_SELECT,
|
||||
"option": "{{ option }}",
|
||||
},
|
||||
},
|
||||
"optimistic": True,
|
||||
"icon": "{% if (trigger.to_state.state or '') == 'a' %}mdi:greater{% else %}mdi:less{% endif %}",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
INPUT_SELECT_DOMAIN,
|
||||
INPUT_SELECT_SERVICE_SELECT_OPTION,
|
||||
{CONF_ENTITY_ID: _OPTION_INPUT_SELECT, INPUT_SELECT_ATTR_OPTION: "b"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(_TEST_SELECT)
|
||||
assert state is not None
|
||||
assert state.state == "b"
|
||||
assert state.attributes[ATTR_ICON] == "mdi:less"
|
||||
|
||||
await hass.services.async_call(
|
||||
INPUT_SELECT_DOMAIN,
|
||||
INPUT_SELECT_SERVICE_SELECT_OPTION,
|
||||
{CONF_ENTITY_ID: _OPTION_INPUT_SELECT, INPUT_SELECT_ATTR_OPTION: "a"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(_TEST_SELECT)
|
||||
assert state.state == "a"
|
||||
assert state.attributes[ATTR_ICON] == "mdi:greater"
|
||||
assert state.attributes[attribute] == expected
|
||||
|
||||
|
||||
async def test_device_id(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user