Compare commits

..

55 Commits

Author SHA1 Message Date
Paul Bottein
df8925b1b9 Update homeassistant/helpers/selector.py
Co-authored-by: G Johansson <goran.johansson@shiftit.se>
2025-06-21 18:15:53 +02:00
Paul Bottein
d61cf0f805 Update format 2025-06-20 14:53:01 +02:00
Paul Bottein
272837205c Add schema supports to object selector 2025-06-20 11:17:18 +02:00
Raphael Hehl
956f726ef3 Bump uiprotect to version 7.14.0 (#147102) 2025-06-19 11:20:29 +02:00
epenet
fada81e1ce Bump ovoenergy to 2.0.1 (#147112) 2025-06-19 08:46:03 +02:00
Simon Lamon
6a16424bb4 Fix nightly build (#147110)
Update builder.yml
2025-06-19 08:20:19 +02:00
Abílio Costa
f90a740429 Use non-autospec mock for Reolink's binary_sensor, camera and diag tests (#147095) 2025-06-19 08:03:48 +02:00
Michael Hansen
3dba7e5bd2 Send intent progress events to ESPHome (#146966) 2025-06-18 22:12:37 -04:00
Erik Montnemery
8d8ff011fc Minor improvements of service helper (#147079) 2025-06-19 00:17:12 +01:00
Michael Hansen
6befd065a1 Bump aioesphomeapi to 32.2.4 (#147100)
Bump aioesphomeapi
2025-06-18 15:49:44 -05:00
Abílio Costa
9adf493acd Use non-autospec mock for Reolink's init tests (#146991) 2025-06-18 17:58:50 +01:00
Michael Hansen
a29d5fb56c tts_output is optional in run-start (#147092) 2025-06-18 12:08:53 -04:00
Petro31
bcb87cf812 Support variables, icon, and picture for all compatible template platforms (#145893)
* Fix template entity variables in blueprints

* add picture and icon tests

* add variable test for all platforms

* apply comments

* Update all test names
2025-06-18 16:49:46 +02:00
Jan Bouwhuis
d01758cea8 Ensure mqtt sensor has a valid native unit of measurement (#146722) 2025-06-18 15:48:38 +02:00
Joakim Sørensen
5487bfe1d9 Bump hass-nabucasa from 0.101.0 to 0.102.0 (#147087) 2025-06-18 15:47:01 +02:00
Simone Chemelli
fec65f40fc Bump aioamazondevices to 3.1.12 (#147055)
* Bump aioamazondevices to 3.1.10

* bump to 3.1.12
2025-06-18 10:20:51 +02:00
Guido Schmitz
596951ea9f Cleanup devolo Home Control tests (#147051) 2025-06-18 09:24:09 +02:00
Norbert Rittel
75d6b885cf Fix typo in state name references of homee (#146905)
Fix typo in state references

Replace wrong semicolons with colon.
2025-06-18 09:23:37 +02:00
Guido Schmitz
3fad76dfa1 Use missed typed ConfigEntry in devolo Home Control (#147049) 2025-06-18 09:22:37 +02:00
Pete Sage
43d8a151ab Remove internals from Sonos test_init.py (#147063)
* fix: test init

* fix: revert

* fix: revert

* fix: revert

* fix: revert

* fix: simplify
2025-06-18 09:21:21 +02:00
starkillerOG
07110e288d If no Reolink HTTP api available, do not set configuration_url (#146684)
* If no http api available, do not set configuration_url

* Add tests
2025-06-18 09:16:08 +02:00
Jan-Philipp Benecke
ba2aac4614 Bump aiowebdav2 to 0.4.6 (#147054) 2025-06-18 09:15:27 +02:00
msw
3449dae7a2 Capitalize "Ice Bites" and switch to "Cubed ice" (#147060) (#147061) 2025-06-18 09:14:45 +02:00
G Johansson
b8cd3f3635 Bump holidays lib to 0.75 (#147043) 2025-06-18 10:11:01 +03:00
Martin Hjelmare
be53ad5449 Disable Z-Wave idle notification button (#147026)
* Update test

* Disable Z-Wave idle notification button

* Update tests
2025-06-18 08:29:04 +03:00
J. Diego Rodríguez Royo
ffd940e07c Set quality scale at Home Connect manifest (#147050) 2025-06-17 21:42:40 +01:00
Josef Zweck
5e31b5ac4f Handle missing widget in lamarzocco (#147047) 2025-06-17 21:25:27 +02:00
puddly
81257f9d57 Bump ZHA to 0.0.60 (#147045) 2025-06-17 22:06:53 +03:00
Josef Zweck
ce1678719a Bump pylamarzocco to 2.0.9 (#147046) 2025-06-17 20:59:41 +02:00
Guido Schmitz
fc6844b3c9 Add _attr_has_entity_name to devolo Home Network device tracker platform (#146978)
* Add _attr_has_entity_name to devolo Home Network device tracker platform

* Set name

* Fix tests
2025-06-17 20:49:52 +02:00
J. Diego Rodríguez Royo
8e82e3aa3a Bump aiohomeconnect to 0.18.0 (#147044) 2025-06-17 20:48:09 +02:00
G Johansson
3bc68941e6 Remove not used constant in climate (#147041) 2025-06-17 20:43:16 +02:00
Josef Zweck
e69b38ab2c Fix log in onedrive (#147029) 2025-06-17 19:57:52 +02:00
Abílio Costa
ed9503324d Fix flaky Reolink webhook test (#147036) 2025-06-17 17:18:48 +01:00
Allen Porter
22a06a6c2e Bump ical to 10.0.4 (#147005)
* Bump ical to 10.0.4

* Bump ical to 10.0.4 in google
2025-06-17 07:06:51 -07:00
Michael Hansen
3b611b9b03 Add TTS response timeout for idle state (#146984)
* Add TTS response timeout for idle state

* Consider time spent sending TTS audio in timeout
2025-06-17 09:39:18 -04:00
Noah Husby
79cc3bffc6 Bump aiorussound to 4.6.0 (#147023) 2025-06-17 14:40:56 +02:00
Martin Hjelmare
5c455304a5 Disable Z-Wave indidator CC entities by default (#147018)
* Update discovery tests

* Disable Z-Wave indidator CC entities by default
2025-06-17 15:39:22 +03:00
Erik Montnemery
058f860be7 Fix incorrect use of zip in service.async_get_all_descriptions (#147013)
* Fix incorrect use of zip in service.async_get_all_descriptions

* Fix lint errors in test
2025-06-17 14:24:31 +02:00
Joost Lekkerkerker
ef319c966d Bump nextcord to 3.1.0 (#147020) 2025-06-17 14:11:55 +02:00
Robin Lintermann
adc4e9fdc1 Bump pysmarlaapi version to 0.9.0 (#146629)
Bump pysmarlaapi version
Fix default values of entities
2025-06-17 11:23:50 +02:00
Maciej Bieniek
40a00fb790 Address late review for NextDNS integration (#146980)
key instead of Key
2025-06-17 11:23:03 +02:00
G Johansson
0926b16095 Remove deprecated support feature values in cover (#146987) 2025-06-17 10:46:08 +02:00
G Johansson
308c89af4a Remove deprecated support feature values in media_player (#146986) 2025-06-17 10:33:41 +02:00
G Johansson
b0c2a47288 Remove deprecated support feature values in vacuum (#146982) 2025-06-17 10:32:58 +02:00
Joost Lekkerkerker
c446cce2cc Bump pySmartThings to 3.2.5 (#146983) 2025-06-16 22:44:14 +01:00
Abílio Costa
e02267ad89 Improve bootstrap file logging test (#146670) 2025-06-16 21:55:16 +01:00
Thomas55555
36381e6753 Bump aioautomower to 2025.6.0 (#146979) 2025-06-16 22:52:23 +02:00
Manu
6533562f4e Rename Xiaomi Miio integration to Xiaomi Home (#146555)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-06-16 21:51:54 +01:00
Ludovic BOUÉ
1bc6ea98ce Set Matter SolarPower tagList in fixture (#146837)
Update solar_power.json

Set tagList to [{"0":null,"1":15,"2":2,"3":"Solar"}]
2025-06-16 22:46:27 +02:00
elmurato
bab34b844b Fix blocking open in Minecraft Server (#146820)
Fix blocking open by dnspython
2025-06-16 22:46:11 +02:00
Etienne C.
ad3dac0373 Removed rounding of durations in Here Travel Time sensors (#146838)
* Removed rounding of durations

* Set duration sensors unit to seconds

* Updated Here Travel Time tests

* Update homeassistant/components/here_travel_time/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/here_travel_time/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Updated Here Travel Time tests

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-06-16 22:20:01 +02:00
Maciej Bieniek
c5d93e5456 Fix translation key in NextDNS integration (#146976)
* Fix translation key

* Better wording
2025-06-16 21:37:19 +02:00
J. Diego Rodríguez Royo
ef9b46dce5 Record current IQS state for Home Connect (#131703)
* Home Connect quality scale

* Update current iqs

* Docs rules done

* parallel-updates rule

* Complete appropriate-polling's comment

* Apply suggestions

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-06-16 21:30:06 +02:00
Abílio Costa
6f3ceb83c2 Use non-autospec mock for Reolink's button tests (#146969) 2025-06-16 21:14:02 +02:00
124 changed files with 1858 additions and 1377 deletions

View File

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

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==3.1.4"]
"requirements": ["aioamazondevices==3.1.12"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_push",
"loggers": ["discord"],
"requirements": ["nextcord==2.6.0"]
"requirements": ["nextcord==3.1.0"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",
"requirements": ["aioautomower==2025.5.1"]
"requirements": ["aioautomower==2025.6.0"]
}

View File

@@ -37,5 +37,5 @@
"iot_class": "cloud_push",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==2.0.8"]
"requirements": ["pylamarzocco==2.0.9"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["ovoenergy"],
"requirements": ["ovoenergy==2.0.0"]
"requirements": ["ovoenergy==2.0.1"]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["ical"],
"quality_scale": "silver",
"requirements": ["ical==10.0.0"]
"requirements": ["ical==10.0.4"]
}

View File

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

View File

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

View File

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

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmarlaapi", "pysignalr"],
"quality_scale": "bronze",
"requirements": ["pysmarlaapi==0.8.2"]
"requirements": ["pysmarlaapi==0.9.0"]
}

View File

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

View File

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

View File

@@ -30,5 +30,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"quality_scale": "bronze",
"requirements": ["pysmartthings==3.2.4"]
"requirements": ["pysmartthings==3.2.5"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aiowebdav2"],
"quality_scale": "bronze",
"requirements": ["aiowebdav2==0.4.5"]
"requirements": ["aiowebdav2==0.4.6"]
}

View File

@@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.74"]
"requirements": ["holidays==0.75"]
}

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,7 @@
"zha",
"universal_silabs_flasher"
],
"requirements": ["zha==0.0.59"],
"requirements": ["zha==0.0.60"],
"usb": [
{
"vid": "10C4",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": [],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -53,6 +53,6 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1',
'state': '1.0',
})
# ---

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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