Compare commits

..

4 Commits

Author SHA1 Message Date
Petar Petrov
03b58a4c21 Support for hierarchy of water meters 2025-10-07 14:09:45 +03:00
FMKaiba
53d1bbb530 Add support for gas detector status to SmartThings (#153831)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-10-07 12:56:53 +02:00
Tom Matheussen
a3ef55274e Add missing translation string for Satel Integra subentry type (#153905) 2025-10-07 12:18:51 +02:00
Joost Lekkerkerker
2034915457 Add fixture to SmartThings (#153902) 2025-10-07 12:13:12 +02:00
14 changed files with 836 additions and 303 deletions

View File

@@ -116,6 +116,10 @@ class WaterSourceType(TypedDict):
# an EnergyCostSensor will be automatically created
stat_cost: str | None
# An optional statistic_id identifying a device
# that includes this device's consumption in its total
included_in_stat: str | None
# Used to generate costs if stat_cost is set to None
entity_energy_price: str | None # entity_id of an entity providing price ($/m³)
number_energy_price: float | None # Price for energy ($/m³)

View File

@@ -24,6 +24,7 @@
},
"config_subentries": {
"partition": {
"entry_type": "Partition",
"initiate_flow": {
"user": "Add partition"
},
@@ -57,6 +58,7 @@
}
},
"zone": {
"entry_type": "Zone",
"initiate_flow": {
"user": "Add zone"
},
@@ -91,6 +93,7 @@
}
},
"output": {
"entry_type": "Output",
"initiate_flow": {
"user": "Add output"
},
@@ -125,6 +128,7 @@
}
},
"switchable_output": {
"entry_type": "Switchable output",
"initiate_flow": {
"user": "Add switchable output"
},

View File

@@ -179,6 +179,13 @@ CAPABILITY_TO_SENSORS: dict[
is_on_key="open",
)
},
Capability.GAS_DETECTOR: {
Attribute.GAS: SmartThingsBinarySensorEntityDescription(
key=Attribute.GAS,
device_class=BinarySensorDeviceClass.GAS,
is_on_key="detected",
)
},
}

View File

@@ -530,7 +530,6 @@ CAPABILITY_TO_SENSORS: dict[
)
],
},
# Haven't seen at devices yet
Capability.ILLUMINANCE_MEASUREMENT: {
Attribute.ILLUMINANCE: [
SmartThingsSensorEntityDescription(
@@ -1010,7 +1009,6 @@ CAPABILITY_TO_SENSORS: dict[
)
]
},
# Haven't seen at devices yet
Capability.ULTRAVIOLET_INDEX: {
Attribute.ULTRAVIOLET_INDEX: [
SmartThingsSensorEntityDescription(

View File

@@ -799,15 +799,14 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]):
without using async_show_progress_done.
If no next step is set, abort the flow.
"""
if (next_step_result := self._progress_step_data["next_step_result"]) is None:
if self._progress_step_data["next_step_result"] is None:
return self.async_abort(
reason=self._progress_step_data["abort_reason"],
description_placeholders=self._progress_step_data[
"abort_description_placeholders"
],
)
self._progress_step_data["next_step_result"] = None
return next_step_result
return self._progress_step_data["next_step_result"]
@callback
def async_external_step(
@@ -1045,7 +1044,7 @@ def progress_step[
# Task is done or this is a subsequent call
try:
progress_task_result = await progress_task
self._progress_step_data["next_step_result"] = await progress_task
except AbortFlow as err:
self._progress_step_data["abort_reason"] = err.reason
self._progress_step_data["abort_description_placeholders"] = (
@@ -1058,9 +1057,6 @@ def progress_step[
# Clean up task reference
self._progress_step_data["tasks"].pop(step_id, None)
if progress_task_result["type"] != FlowResultType.SHOW_PROGRESS_DONE:
self._progress_step_data["next_step_result"] = progress_task_result
return self.async_show_progress_done(
next_step_id="_progress_step_progress_done"
)

View File

@@ -97,6 +97,7 @@ def mock_smartthings() -> Generator[AsyncMock]:
@pytest.fixture(
params=[
"aq_sensor_3_ikea",
"aeotec_ms6",
"da_ac_airsensor_01001",
"da_ac_rac_000001",
"da_ac_rac_000003",
@@ -156,6 +157,7 @@ def mock_smartthings() -> Generator[AsyncMock]:
"heatit_ztrm3_thermostat",
"heatit_zpushwall",
"generic_ef00_v1",
"gas_detector",
"bosch_radiator_thermostat_ii",
"im_speaker_ai_0001",
"im_smarttag2_ble_uwb",

View File

@@ -0,0 +1,62 @@
{
"components": {
"main": {
"ultravioletIndex": {
"ultravioletIndex": {
"value": 0,
"timestamp": "2025-09-30T15:13:46.521Z"
}
},
"relativeHumidityMeasurement": {
"humidity": {
"value": 60.0,
"unit": "%",
"timestamp": "2025-09-30T15:13:45.441Z"
}
},
"temperatureMeasurement": {
"temperatureRange": {
"value": null
},
"temperature": {
"value": 22.2,
"unit": "C",
"timestamp": "2025-09-30T16:13:50.478Z"
}
},
"refresh": {},
"motionSensor": {
"motion": {
"value": "inactive",
"timestamp": "2025-09-30T15:33:27.594Z"
}
},
"illuminanceMeasurement": {
"illuminance": {
"value": 30,
"unit": "lux",
"timestamp": "2025-09-30T15:13:52.607Z"
}
},
"battery": {
"quantity": {
"value": null
},
"battery": {
"value": 100,
"unit": "%",
"timestamp": "2025-09-30T15:13:46.166Z"
},
"type": {
"value": null
}
},
"tamperAlert": {
"tamper": {
"value": "clear",
"timestamp": "2025-09-30T14:06:07.064Z"
}
}
}
}
}

View File

@@ -0,0 +1,25 @@
{
"components": {
"main": {
"momentary": {},
"gasDetector": {
"gas": {
"value": "clear",
"timestamp": "2025-10-02T03:18:27.139Z"
}
},
"signalStrength": {
"rssi": {
"value": -71,
"unit": "dBm",
"timestamp": "2025-10-07T04:17:08.419Z"
},
"lqi": {
"value": 148,
"timestamp": "2025-10-07T04:32:08.512Z"
}
},
"refresh": {}
}
}
}

View File

@@ -0,0 +1,86 @@
{
"items": [
{
"deviceId": "00f9233e-fdaa-4020-99d4-e0073e53996a",
"name": "aeotec-ms6",
"label": "Parent's Bedroom Sensor",
"manufacturerName": "SmartThingsCommunity",
"presentationId": "6d160aa8-7f54-3611-b7de-0b335d162529",
"deviceManufacturerCode": "0086-0102-0064",
"locationId": "3478ae40-8bd4-40b8-b7e6-f25e3cf86409",
"ownerId": "fe7f9079-8e23-8307-fb7e-4d58929391cf",
"roomId": "f1bb7871-3a3d-48da-b23f-0e1297e8acb0",
"components": [
{
"id": "main",
"label": "main",
"capabilities": [
{
"id": "motionSensor",
"version": 1
},
{
"id": "temperatureMeasurement",
"version": 1
},
{
"id": "relativeHumidityMeasurement",
"version": 1
},
{
"id": "illuminanceMeasurement",
"version": 1
},
{
"id": "ultravioletIndex",
"version": 1
},
{
"id": "tamperAlert",
"version": 1
},
{
"id": "battery",
"version": 1
},
{
"id": "refresh",
"version": 1
}
],
"categories": [
{
"name": "MotionSensor",
"categoryType": "manufacturer"
}
],
"optional": false
}
],
"createTime": "2025-04-17T05:47:05.803Z",
"parentDeviceId": "9fdfde11-206e-47af-9e47-9c314d8d965f",
"profile": {
"id": "9893d370-2af6-32a0-86c5-f1a6d2b9fea7"
},
"zwave": {
"networkId": "BE",
"driverId": "42930682-019d-4dbe-8098-760d7afb3c7f",
"executingLocally": true,
"hubId": "9fdfde11-206e-47af-9e47-9c314d8d965f",
"networkSecurityLevel": "ZWAVE_LEGACY_NON_SECURE",
"provisioningState": "PROVISIONED",
"manufacturerId": 134,
"productType": 258,
"productId": 100,
"fingerprintType": "ZWAVE_MANUFACTURER",
"fingerprintId": "Aeotec/MS6/US"
},
"type": "ZWAVE",
"restrictionTier": 0,
"allowed": null,
"executionContext": "LOCAL",
"relationships": []
}
],
"_links": {}
}

View File

@@ -0,0 +1,66 @@
{
"items": [
{
"deviceId": "d830b46f-f094-4560-b8c3-7690032fdb4c",
"name": "generic-ef00-v1",
"label": "Gas Detector",
"manufacturerName": "SmartThingsCommunity",
"presentationId": "d4b88195-fd5b-39d3-ac6f-7070655f08ab",
"deviceManufacturerCode": "_TZE284_chbyv06x",
"locationId": "7139bb09-31e3-4fad-bf03-b9ad02e57b41",
"ownerId": "00126705-d35b-27ee-d18b-17620d9929e7",
"roomId": "5adccb3a-8ae7-41c0-bc58-7ba80ff78a18",
"components": [
{
"id": "main",
"label": "Detector",
"capabilities": [
{
"id": "gasDetector",
"version": 1
},
{
"id": "momentary",
"version": 1
},
{
"id": "signalStrength",
"version": 1
},
{
"id": "refresh",
"version": 1
}
],
"categories": [
{
"name": "Siren",
"categoryType": "manufacturer"
}
],
"optional": false
}
],
"createTime": "2025-05-25T04:55:42.440Z",
"profile": {
"id": "1d34dd9d-6840-3df6-a6d0-5d9f4a4af2e1"
},
"zigbee": {
"eui": "A4C138C524A5BC8D",
"networkId": "1575",
"driverId": "bc7fd1bc-eb00-4b7f-8977-172acf823508",
"executingLocally": true,
"hubId": "0afe704f-eabb-4e4d-8333-6c73903e4f84",
"provisioningState": "DRIVER_SWITCH",
"fingerprintType": "ZIGBEE_GENERIC",
"fingerprintId": "GenericEF00"
},
"type": "ZIGBEE",
"restrictionTier": 0,
"allowed": null,
"executionContext": "LOCAL",
"relationships": []
}
],
"_links": {}
}

View File

@@ -1,4 +1,102 @@
# serializer version: 1
# name: test_all_entities[aeotec_ms6][binary_sensor.parent_s_bedroom_sensor_motion-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': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.parent_s_bedroom_sensor_motion',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.MOTION: 'motion'>,
'original_icon': None,
'original_name': 'Motion',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_motionSensor_motion_motion',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[aeotec_ms6][binary_sensor.parent_s_bedroom_sensor_motion-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'motion',
'friendly_name': "Parent's Bedroom Sensor Motion",
}),
'context': <ANY>,
'entity_id': 'binary_sensor.parent_s_bedroom_sensor_motion',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[aeotec_ms6][binary_sensor.parent_s_bedroom_sensor_tamper-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': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.parent_s_bedroom_sensor_tamper',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.TAMPER: 'tamper'>,
'original_icon': None,
'original_name': 'Tamper',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_tamperAlert_tamper_tamper',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[aeotec_ms6][binary_sensor.parent_s_bedroom_sensor_tamper-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'tamper',
'friendly_name': "Parent's Bedroom Sensor Tamper",
}),
'context': <ANY>,
'entity_id': 'binary_sensor.parent_s_bedroom_sensor_tamper',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[c2c_arlo_pro_3_switch][binary_sensor.2nd_floor_hallway_motion-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -2572,6 +2670,55 @@
'state': 'off',
})
# ---
# name: test_all_entities[gas_detector][binary_sensor.gas_detector_gas-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': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.gas_detector_gas',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.GAS: 'gas'>,
'original_icon': None,
'original_name': 'Gas',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'd830b46f-f094-4560-b8c3-7690032fdb4c_main_gasDetector_gas_gas',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[gas_detector][binary_sensor.gas_detector_gas-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'gas',
'friendly_name': 'Gas Detector Gas',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.gas_detector_gas',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[iphone][binary_sensor.iphone_presence-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -64,6 +64,37 @@
'via_device_id': None,
})
# ---
# name: test_devices[aeotec_ms6]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://account.smartthings.com',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'00f9233e-fdaa-4020-99d4-e0073e53996a',
),
}),
'labels': set({
}),
'manufacturer': None,
'model': None,
'model_id': None,
'name': "Parent's Bedroom Sensor",
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': None,
})
# ---
# name: test_devices[aq_sensor_3_ikea]
DeviceRegistryEntrySnapshot({
'area_id': None,
@@ -1304,6 +1335,37 @@
'via_device_id': None,
})
# ---
# name: test_devices[gas_detector]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://account.smartthings.com',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'd830b46f-f094-4560-b8c3-7690032fdb4c',
),
}),
'labels': set({
}),
'manufacturer': None,
'model': None,
'model_id': None,
'name': 'Gas Detector',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': None,
})
# ---
# name: test_devices[gas_meter]
DeviceRegistryEntrySnapshot({
'area_id': None,

View File

@@ -163,6 +163,269 @@
'state': 'unknown',
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_battery-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': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.parent_s_bedroom_sensor_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_battery_battery_battery',
'unit_of_measurement': '%',
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': "Parent's Bedroom Sensor Battery",
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.parent_s_bedroom_sensor_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_humidity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.parent_s_bedroom_sensor_humidity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
'original_icon': None,
'original_name': 'Humidity',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_relativeHumidityMeasurement_humidity_humidity',
'unit_of_measurement': '%',
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_humidity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'humidity',
'friendly_name': "Parent's Bedroom Sensor Humidity",
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.parent_s_bedroom_sensor_humidity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '60.0',
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_illuminance-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.parent_s_bedroom_sensor_illuminance',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ILLUMINANCE: 'illuminance'>,
'original_icon': None,
'original_name': 'Illuminance',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_illuminanceMeasurement_illuminance_illuminance',
'unit_of_measurement': 'lx',
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_illuminance-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'illuminance',
'friendly_name': "Parent's Bedroom Sensor Illuminance",
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'lx',
}),
'context': <ANY>,
'entity_id': 'sensor.parent_s_bedroom_sensor_illuminance',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '30',
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.parent_s_bedroom_sensor_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Temperature',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_temperatureMeasurement_temperature_temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': "Parent's Bedroom Sensor Temperature",
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.parent_s_bedroom_sensor_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '22.2',
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_uv_index-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.parent_s_bedroom_sensor_uv_index',
'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': 'UV index',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'uv_index',
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_ultravioletIndex_ultravioletIndex_ultravioletIndex',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_uv_index-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': "Parent's Bedroom Sensor UV index",
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.parent_s_bedroom_sensor_uv_index',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_all_entities[aq_sensor_3_ikea][sensor.aq_sensor_3_ikea_humidity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -12651,6 +12914,110 @@
'state': 'unknown',
})
# ---
# name: test_all_entities[gas_detector][sensor.gas_detector_link_quality-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.gas_detector_link_quality',
'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': 'Link quality',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'link_quality',
'unique_id': 'd830b46f-f094-4560-b8c3-7690032fdb4c_main_signalStrength_lqi_lqi',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[gas_detector][sensor.gas_detector_link_quality-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Gas Detector Link quality',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gas_detector_link_quality',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '148',
})
# ---
# name: test_all_entities[gas_detector][sensor.gas_detector_signal_strength-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.gas_detector_signal_strength',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.SIGNAL_STRENGTH: 'signal_strength'>,
'original_icon': None,
'original_name': 'Signal strength',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'd830b46f-f094-4560-b8c3-7690032fdb4c_main_signalStrength_rssi_rssi',
'unit_of_measurement': 'dBm',
})
# ---
# name: test_all_entities[gas_detector][sensor.gas_detector_signal_strength-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'signal_strength',
'friendly_name': 'Gas Detector Signal strength',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'dBm',
}),
'context': <ANY>,
'entity_id': 'sensor.gas_detector_signal_strength',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '-71',
})
# ---
# name: test_all_entities[gas_meter][sensor.gas_meter_gas-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -1,11 +1,9 @@
"""Test the flow classes."""
import asyncio
from collections.abc import Callable
import dataclasses
import logging
from typing import Any
from unittest.mock import AsyncMock, Mock, patch
from unittest.mock import Mock, patch
import pytest
import voluptuous as vol
@@ -932,297 +930,6 @@ async def test_show_progress_fires_only_when_changed(
) # change (description placeholder)
@pytest.mark.parametrize(
("task_side_effect", "flow_result"),
[
(None, data_entry_flow.FlowResultType.CREATE_ENTRY),
(data_entry_flow.AbortFlow("fail"), data_entry_flow.FlowResultType.ABORT),
],
)
@pytest.mark.parametrize(
("description", "expected_description"),
[
(None, None),
({"title": "World"}, {"title": "World"}),
(lambda x: {"title": "World"}, {"title": "World"}),
],
)
async def test_progress_step(
hass: HomeAssistant,
manager: MockFlowManager,
description: Callable[[data_entry_flow.FlowHandler], dict[str, Any]]
| dict[str, Any]
| None,
expected_description: dict[str, Any] | None,
task_side_effect: Exception | None,
flow_result: data_entry_flow.FlowResultType,
) -> None:
"""Test progress_step decorator."""
manager.hass = hass
events = []
task_init_evt = asyncio.Event()
event_received_evt = asyncio.Event()
task_result = Mock()
task_result.side_effect = task_side_effect
@callback
def capture_events(event: Event) -> None:
events.append(event)
event_received_evt.set()
@manager.mock_reg_handler("test")
class TestFlow(data_entry_flow.FlowHandler):
VERSION = 5
@data_entry_flow.progress_step(description_placeholders=description)
async def async_step_init(self, user_input=None):
await task_init_evt.wait()
task_result()
return await self.async_step_finish()
async def async_step_finish(self, user_input=None):
return self.async_create_entry(data={})
hass.bus.async_listen(
data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESSED,
capture_events,
)
result = await manager.async_init("test")
assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS
assert result["progress_action"] == "init"
description_placeholders = result["description_placeholders"]
assert description_placeholders == expected_description
assert len(manager.async_progress()) == 1
assert len(manager.async_progress_by_handler("test")) == 1
assert manager.async_get(result["flow_id"])["handler"] == "test"
# Set task one done and wait for event
task_init_evt.set()
await event_received_evt.wait()
event_received_evt.clear()
assert len(events) == 1
assert events[0].data == {
"handler": "test",
"flow_id": result["flow_id"],
"refresh": True,
}
# Frontend refreshes the flow
result = await manager.async_configure(result["flow_id"])
assert result["type"] == flow_result
@pytest.mark.parametrize(
(
"task_init_side_effect", # side effect for initial step task
"task_next_side_effect", # side effect for next step task
"flow_result_before_init", # result before init task is done
"flow_result_after_init", # result after init task is done
"flow_result_after_next", # result after next task is done
"flow_init_events", # number of events fired after init task is done
"flow_next_events", # number of events fired after next task is done
"manager_call_after_init", # lambda to continue the flow after init task
"manager_call_after_next", # lambda to continue the flow after next task
"before_init_task_side_effect", # function called before init event
"before_next_task_side_effect", # function called before next event
),
[
( # both steps show progress and complete successfully
None,
None,
data_entry_flow.FlowResultType.SHOW_PROGRESS,
data_entry_flow.FlowResultType.SHOW_PROGRESS,
data_entry_flow.FlowResultType.CREATE_ENTRY,
1,
2,
lambda manager, result: manager.async_configure(result["flow_id"]),
lambda manager, result: manager.async_configure(result["flow_id"]),
lambda received_event, init_task_event, next_task_event: None,
lambda received_event, init_task_event, next_task_event: None,
),
( # first step aborts
data_entry_flow.AbortFlow("fail"),
None,
data_entry_flow.FlowResultType.SHOW_PROGRESS,
data_entry_flow.FlowResultType.ABORT,
data_entry_flow.FlowResultType.ABORT,
1,
1,
lambda manager, result: manager.async_configure(result["flow_id"]),
lambda manager, result: AsyncMock(return_value=result)(),
lambda received_event, init_task_event, next_task_event: None,
lambda received_event, init_task_event, next_task_event: None,
),
( # first step shows progress, second step aborts
None,
data_entry_flow.AbortFlow("fail"),
data_entry_flow.FlowResultType.SHOW_PROGRESS,
data_entry_flow.FlowResultType.SHOW_PROGRESS,
data_entry_flow.FlowResultType.ABORT,
1,
2,
lambda manager, result: manager.async_configure(result["flow_id"]),
lambda manager, result: manager.async_configure(result["flow_id"]),
lambda received_event, init_task_event, next_task_event: None,
lambda received_event, init_task_event, next_task_event: None,
),
( # first step shows progress and second step task is already done
None,
None,
data_entry_flow.FlowResultType.SHOW_PROGRESS,
data_entry_flow.FlowResultType.CREATE_ENTRY,
data_entry_flow.FlowResultType.CREATE_ENTRY,
1,
1,
lambda manager, result: manager.async_configure(result["flow_id"]),
lambda manager, result: AsyncMock(return_value=result)(),
lambda received_event,
init_task_event,
next_task_event: next_task_event.set(),
lambda received_event, init_task_event, next_task_event: None,
),
( # both step tasks are already done and flow completes immediately
None,
None,
data_entry_flow.FlowResultType.SHOW_PROGRESS_DONE,
data_entry_flow.FlowResultType.CREATE_ENTRY,
data_entry_flow.FlowResultType.CREATE_ENTRY,
0,
0,
lambda manager, result: manager.async_configure(result["flow_id"]),
lambda manager, result: AsyncMock(return_value=result)(),
lambda received_event,
init_task_event,
next_task_event: received_event.set()
or init_task_event.set()
or next_task_event.set(),
lambda received_event,
init_task_event,
next_task_event: received_event.set(),
),
( # first step task is already done, second step shows progress and completes
None,
None,
data_entry_flow.FlowResultType.SHOW_PROGRESS_DONE,
data_entry_flow.FlowResultType.SHOW_PROGRESS,
data_entry_flow.FlowResultType.CREATE_ENTRY,
0,
0,
lambda manager, result: manager.async_configure(result["flow_id"]),
lambda manager, result: manager.async_configure(result["flow_id"]),
lambda received_event,
init_task_event,
next_task_event: received_event.set() or init_task_event.set(),
lambda received_event,
init_task_event,
next_task_event: received_event.set(),
),
],
)
async def test_chaining_progress_steps(
hass: HomeAssistant,
manager: MockFlowManager,
task_init_side_effect: Exception | None,
task_next_side_effect: Exception | None,
flow_result_before_init: data_entry_flow.FlowResultType,
flow_result_after_init: data_entry_flow.FlowResultType,
flow_result_after_next: data_entry_flow.FlowResultType,
flow_init_events: int,
flow_next_events: int,
manager_call_after_init: Callable[
[MockFlowManager, data_entry_flow.FlowResult], Any
],
manager_call_after_next: Callable[
[MockFlowManager, data_entry_flow.FlowResult], Any
],
before_init_task_side_effect: Callable[
[asyncio.Event, asyncio.Event, asyncio.Event], None
],
before_next_task_side_effect: Callable[
[asyncio.Event, asyncio.Event, asyncio.Event], None
],
) -> None:
"""Test chaining two steps with progress_step decorators."""
manager.hass = hass
events = []
event_received_evt = asyncio.Event()
task_init_evt = asyncio.Event()
task_next_evt = asyncio.Event()
task_init_result = Mock()
task_init_result.side_effect = task_init_side_effect
task_next_result = Mock()
task_next_result.side_effect = task_next_side_effect
@callback
def capture_events(event: Event) -> None:
events.append(event)
event_received_evt.set()
@manager.mock_reg_handler("test")
class TestFlow(data_entry_flow.FlowHandler):
VERSION = 5
def async_remove(self) -> None:
# Disable event received event to allow test to finish if flow is aborted.
event_received_evt.set()
@data_entry_flow.progress_step()
async def async_step_init(self, user_input=None):
await task_init_evt.wait()
task_init_result()
return await self.async_step_next()
@data_entry_flow.progress_step()
async def async_step_next(self, user_input=None):
await task_next_evt.wait()
task_next_result()
return await self.async_step_finish()
async def async_step_finish(self, user_input=None):
return self.async_create_entry(data={})
hass.bus.async_listen(
data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESSED,
capture_events,
)
# Run side effect before first event is awaited
before_init_task_side_effect(event_received_evt, task_init_evt, task_next_evt)
result = await manager.async_init("test")
assert result["type"] == flow_result_before_init
assert len(manager.async_progress()) == 1
assert len(manager.async_progress_by_handler("test")) == 1
assert manager.async_get(result["flow_id"])["handler"] == "test"
# Set task init done and wait for event
task_init_evt.set()
await event_received_evt.wait()
event_received_evt.clear()
assert len(events) == flow_init_events
# Run side effect before second event is awaited
before_next_task_side_effect(event_received_evt, task_init_evt, task_next_evt)
# Continue the flow if needed.
result = await manager_call_after_init(manager, result)
assert result["type"] == flow_result_after_init
# Set task next done and wait for event
task_next_evt.set()
await event_received_evt.wait()
event_received_evt.clear()
assert len(events) == flow_next_events
# Continue the flow if needed.
result = await manager_call_after_next(manager, result)
assert result["type"] == flow_result_after_next
async def test_abort_flow_exception_step(manager: MockFlowManager) -> None:
"""Test that the AbortFlow exception works in a step."""