diff --git a/.core_files.yaml b/.core_files.yaml index d2d2ae9780b5a..72bbba323336b 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -118,10 +118,13 @@ components: &components - homeassistant/components/sun/** - homeassistant/components/system_health/** - homeassistant/components/tag/** + # telegram_bot is used by telegram - homeassistant/components/telegram_bot/** - homeassistant/components/template/** - homeassistant/components/timer/** - homeassistant/components/trace/** + # twilio is used by twilio_call and twilio_sms + - homeassistant/components/twilio/** - homeassistant/components/usb/** - homeassistant/components/webhook/** - homeassistant/components/websocket_api/** diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 6e3a3f0c1a5ad..dc7e6b10af920 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -65,7 +65,7 @@ jobs: persist-credentials: false - name: Set up pnpm - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 with: package_json_file: tests/e2e/package.json diff --git a/CODEOWNERS b/CODEOWNERS index 46e9d80844b24..eb25e9ba74d4f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -2054,8 +2054,6 @@ CLAUDE.md @home-assistant/core /tests/components/volumio/ @OnFreund /homeassistant/components/volvo/ @thomasddn /tests/components/volvo/ @thomasddn -/homeassistant/components/wake_on_lan/ @ntilley905 -/tests/components/wake_on_lan/ @ntilley905 /homeassistant/components/wake_word/ @home-assistant/core @synesthesiam /tests/components/wake_word/ @home-assistant/core @synesthesiam /homeassistant/components/wallbox/ @hesselonline diff --git a/homeassistant/components/besen/manifest.json b/homeassistant/components/besen/manifest.json index 531b1d7e5408c..f575d9b0f4dde 100644 --- a/homeassistant/components/besen/manifest.json +++ b/homeassistant/components/besen/manifest.json @@ -14,5 +14,5 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "bronze", - "requirements": ["besen==0.4.0"] + "requirements": ["besen==0.4.2"] } diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py index e73b694d5f876..b691abd7e851e 100644 --- a/homeassistant/components/izone/__init__.py +++ b/homeassistant/components/izone/__init__.py @@ -204,9 +204,6 @@ def _async_on_address_changed(endpoint: pizone.ControllerEndpoint) -> None: async def async_migrate_entry(hass: HomeAssistant, entry: IZoneConfigEntry) -> bool: """Migrate old config entry schema to the current version.""" if entry.version == 1: - # Clear legacy data only. - # Raising ConfigEntryNotReady from async_migrate_entry would permanently land - # the entry in MIGRATION_ERROR with no retry path. hass.config_entries.async_update_entry(entry, version=2, data={}) return True return False diff --git a/homeassistant/components/midea/binary_sensor.py b/homeassistant/components/midea/binary_sensor.py index be553eb38a7fa..5b85c13e5efa7 100644 --- a/homeassistant/components/midea/binary_sensor.py +++ b/homeassistant/components/midea/binary_sensor.py @@ -42,6 +42,12 @@ device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, ), + BinarySensorEntityDescription( + key="water_pump_running", + translation_key="water_pump_running", + device_class=BinarySensorDeviceClass.RUNNING, + entity_category=EntityCategory.DIAGNOSTIC, + ), BinarySensorEntityDescription( key="filter_cleaning_reminder", translation_key="filter_cleaning_reminder", diff --git a/homeassistant/components/midea/climate.py b/homeassistant/components/midea/climate.py index 67bbe271ac942..f32ffcc16c603 100644 --- a/homeassistant/components/midea/climate.py +++ b/homeassistant/components/midea/climate.py @@ -718,7 +718,7 @@ def __init__( ) -> None: """Midea FB Climate entity init.""" super().__init__(device, description) - self._attr_preset_modes: list[str] = self._device.modes + self._attr_preset_modes: list[str] = cast(list[str], self._device.modes) @property @override diff --git a/homeassistant/components/midea/icons.json b/homeassistant/components/midea/icons.json index 5b0e88519d739..1d09a42c062e1 100644 --- a/homeassistant/components/midea/icons.json +++ b/homeassistant/components/midea/icons.json @@ -3,6 +3,9 @@ "binary_sensor": { "header_exist": { "default": "mdi:air-filter" + }, + "water_pump_running": { + "default": "mdi:water-pump" } }, "climate": { diff --git a/homeassistant/components/midea/manifest.json b/homeassistant/components/midea/manifest.json index 943d3453e69c3..5ee46fc762e1b 100644 --- a/homeassistant/components/midea/manifest.json +++ b/homeassistant/components/midea/manifest.json @@ -13,5 +13,5 @@ "iot_class": "local_polling", "loggers": ["midealocal"], "quality_scale": "bronze", - "requirements": ["midea-local==11.0.1"] + "requirements": ["midea-local==12.0.0"] } diff --git a/homeassistant/components/midea/strings.json b/homeassistant/components/midea/strings.json index 9a2f0ccb8c268..5a4fc1867b3db 100644 --- a/homeassistant/components/midea/strings.json +++ b/homeassistant/components/midea/strings.json @@ -147,6 +147,9 @@ }, "tank_full": { "name": "Tank full" + }, + "water_pump_running": { + "name": "Water pump running" } }, "button": { diff --git a/homeassistant/components/modbus/connection.py b/homeassistant/components/modbus/connection.py index 84a87d6b3eca9..f78f9e34c334e 100644 --- a/homeassistant/components/modbus/connection.py +++ b/homeassistant/components/modbus/connection.py @@ -34,6 +34,27 @@ ) +def _canonical(params: ModbusParams) -> ModbusParams: + """Return the params in the form the link is actually built from. + + RTU and ASCII frame a serial line, so `ModbusTcpParams` carrying one builds + the serial link over a `socket://` device that `ModbusSerialParams` names + directly. Both spellings of that link have to reach the same connection. + """ + if not isinstance(params, ModbusTcpParams) or params.framer not in ("rtu", "ascii"): + return params + # An IPv6 literal is bracketed, or its own colons read as the port separator. + host = f"[{params.host}]" if ":" in params.host else params.host + return ModbusSerialParams( + device=f"socket://{host}:{params.port}", + framer=params.framer, + # The line speed modbus-connection gives a socket-carried serial framing. + # It is fast enough to land on tmodbus's 1.75 ms inter-frame gap floor, + # which any value at or above 19200 baud also gets. + baudrate=115200, + ) + + @dataclass class _SharedConnection: """A connection and the units held on it.""" @@ -77,6 +98,7 @@ def _async_acquire( Raises `HomeAssistantError` if the device is already in use over different link settings, which cannot both be honoured on one connection. """ + params = _canonical(params) endpoint = params.endpoint connections = hass.data.setdefault(DATA_MODBUS_CONNECTIONS, {}) if (shared := connections.get(endpoint)) is None: diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 3ece80593cff6..4e4915dc82c63 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -7,7 +7,7 @@ "loggers": ["pymodbus"], "requirements": [ "pymodbus==3.13.1", - "modbus-connection[tmodbus]==4.11.1", + "modbus-connection[tmodbus]==4.12.0", "tmodbus==0.6.2" ] } diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index dcdb200d932a1..e8e247545922c 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -370,8 +370,8 @@ async def async_publish_service(call: ServiceCall) -> None: message_expiry_interval=message_expiry_interval, ) - hass.services.async_register( - DOMAIN, SERVICE_PUBLISH, async_publish_service, schema=MQTT_PUBLISH_SCHEMA + async_register_admin_service( + hass, DOMAIN, SERVICE_PUBLISH, async_publish_service, MQTT_PUBLISH_SCHEMA ) async def async_dump_service(call: ServiceCall) -> None: @@ -395,7 +395,8 @@ async def finish_dump(_: datetime) -> None: ev.async_call_later(hass, call.data["duration"], finish_dump) - hass.services.async_register( + async_register_admin_service( + hass, DOMAIN, SERVICE_DUMP, async_dump_service, diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 064f14abc4184..e58723d8a2193 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -98,6 +98,18 @@ def model_requires_encryption(model: str | None) -> bool: return model is not None and len(model) > 4 and model[4] in ("H", "J") +def model_may_require_encryption(model: str | None) -> bool: + """Return True for models that might need encrypted pairing. + + Some 2016 K-series models advertise the plain websocket method in their REST + device info but only pair via the encrypted PIN flow. Unlike H/J models (see + model_requires_encryption), other sets in the same series work with the + websocket method, so the encrypted method is only tried as a fallback once + websocket pairing has failed to connect. + """ + return model is not None and len(model) > 4 and model[4] == "K" + + async def async_get_device_info( hass: HomeAssistant, host: str, diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 32b764d249f23..92b492d77777d 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -39,7 +39,12 @@ ) from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo -from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info +from .bridge import ( + SamsungTVBridge, + async_get_device_info, + mac_from_device_info, + model_may_require_encryption, +) from .const import ( CONF_MANUFACTURER, CONF_SESSION_ID, @@ -47,6 +52,7 @@ CONF_SSDP_RENDERING_CONTROL_LOCATION, DEFAULT_MANUFACTURER, DOMAIN, + ENCRYPTED_WEBSOCKET_PORT, LOGGER, METHOD_ENCRYPTED_WEBSOCKET, METHOD_LEGACY, @@ -324,6 +330,26 @@ async def async_step_pairing( result = await self._bridge.async_try_connect() if result == RESULT_SUCCESS: return self._get_entry_from_bridge() + if result == RESULT_CANNOT_CONNECT and model_may_require_encryption( + self._model + ): + # Some 2016 K-series sets advertise the websocket method but only + # pair via the encrypted PIN flow; try it before giving up + LOGGER.debug( + "Websocket pairing failed for %s (%s), falling back to encrypted", + self._host, + self._model, + ) + encrypted_bridge = SamsungTVBridge.get_bridge( + self.hass, + METHOD_ENCRYPTED_WEBSOCKET, + self._host, + ENCRYPTED_WEBSOCKET_PORT, + ) + if await encrypted_bridge.async_try_connect() == RESULT_CANNOT_CONNECT: + raise AbortFlow(RESULT_CANNOT_CONNECT) + self._bridge = encrypted_bridge + return await self.async_step_encrypted_pairing() if result != RESULT_AUTH_MISSING: raise AbortFlow(result) errors = {"base": RESULT_AUTH_MISSING} diff --git a/homeassistant/components/teslemetry/config_flow.py b/homeassistant/components/teslemetry/config_flow.py index 8d071f9199ee2..3529757397525 100644 --- a/homeassistant/components/teslemetry/config_flow.py +++ b/homeassistant/components/teslemetry/config_flow.py @@ -36,6 +36,7 @@ from homeassistant.components.bluetooth import ( async_discovered_service_info, async_request_active_scan, + async_scanner_count, ) from homeassistant.config_entries import ( SOURCE_REAUTH, @@ -227,6 +228,8 @@ async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> SubentryFlowResult: """Select an account vehicle to add over Bluetooth, then pair it.""" + if not async_scanner_count(self.hass, connectable=True): + return self.async_abort(reason="bluetooth_not_available") entry = self._get_entry() if entry.state is not ConfigEntryState.LOADED: return self.async_abort(reason="entry_not_loaded") diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index 3a477f3c20a8f..43e8704544393 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -89,6 +89,7 @@ "vehicle": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "bluetooth_not_available": "At least one Bluetooth adapter or remote Bluetooth proxy must be configured to add a vehicle over Bluetooth.", "entry_not_loaded": "The Teslemetry configuration entry is not loaded. Please ensure it is set up correctly before adding a vehicle.", "no_vehicles": "Every eligible vehicle in your Teslemetry account has already been added over Bluetooth." }, diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index af29c09a021a0..12a7ff3076f67 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["total_connect_client"], - "requirements": ["total-connect-client==2026.7"] + "requirements": ["total-connect-client==2026.9"] } diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 72d94b28e445e..40cbcf78d237b 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -888,6 +888,7 @@ class DPCode(StrEnum): SWITCH_7 = "switch_7" # Switch 7 SWITCH_8 = "switch_8" # Switch 8 SWITCH_ALARM_LIGHT = "switch_alarm_light" + SWITCH_ALARM_PROPEL = "switch_alarm_propel" SWITCH_ALARM_SOUND = "switch_alarm_sound" SWITCH_BACKLIGHT = "switch_backlight" # Backlight switch SWITCH_CHARGE = "switch_charge" @@ -895,6 +896,7 @@ class DPCode(StrEnum): SWITCH_DISTURB = "switch_disturb" SWITCH_FAN = "switch_fan" SWITCH_HORIZONTAL = "switch_horizontal" # Horizontal swing flap switch + SWITCH_KB_SOUND = "switch_kb_sound" SWITCH_LED = "switch_led" # Switch SWITCH_LED_1 = "switch_led_1" SWITCH_LED_2 = "switch_led_2" diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index acb06214cf94c..6650449929132 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -13,10 +13,16 @@ ) from tuya_device_handlers.device_wrapper.sensor import ( DeltaIntegerWrapper, + ElectricityApparentPowerJsonWrapper, + ElectricityApparentPowerRawWrapper, ElectricityCurrentJsonWrapper, ElectricityCurrentRawWrapper, + ElectricityPowerFactorJsonWrapper, + ElectricityPowerFactorRawWrapper, ElectricityPowerJsonWrapper, ElectricityPowerRawWrapper, + ElectricityReactivePowerJsonWrapper, + ElectricityReactivePowerRawWrapper, ElectricityVoltageJsonWrapper, ElectricityVoltageRawWrapper, WindDirectionEnumWrapper, @@ -56,10 +62,6 @@ from .entity import TuyaEntity from .util import get_device_temp_unit_convert -CURRENT_WRAPPER = (ElectricityCurrentRawWrapper, ElectricityCurrentJsonWrapper) -POWER_WRAPPER = (ElectricityPowerRawWrapper, ElectricityPowerJsonWrapper) -VOLTAGE_WRAPPER = (ElectricityVoltageRawWrapper, ElectricityVoltageJsonWrapper) - @dataclass(frozen=True) class TuyaSensorEntityDescription(SensorEntityDescription): @@ -69,6 +71,72 @@ class TuyaSensorEntityDescription(SensorEntityDescription): wrapper_class: tuple[type[DPCodeTypeInformationWrapper], ...] | None = None +def _electricity_data(dpcode: DPCode) -> tuple[TuyaSensorEntityDescription, ...]: + """Build the sensors extracted from a DPCode holding electricity data. + + The DPCode value doubles as the translation key prefix (e.g. `phase_a`). + """ + return ( + TuyaSensorEntityDescription( + key=f"{dpcode}electriccurrent", + dpcode=dpcode, + translation_key=f"{dpcode}_current", + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + wrapper_class=(ElectricityCurrentRawWrapper, ElectricityCurrentJsonWrapper), + ), + TuyaSensorEntityDescription( + key=f"{dpcode}power", + dpcode=dpcode, + translation_key=f"{dpcode}_power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + wrapper_class=(ElectricityPowerRawWrapper, ElectricityPowerJsonWrapper), + ), + TuyaSensorEntityDescription( + key=f"{dpcode}voltage", + dpcode=dpcode, + translation_key=f"{dpcode}_voltage", + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + wrapper_class=(ElectricityVoltageRawWrapper, ElectricityVoltageJsonWrapper), + ), + TuyaSensorEntityDescription( + key=f"{dpcode}reactivepower", + dpcode=dpcode, + translation_key=f"{dpcode}_reactive_power", + device_class=SensorDeviceClass.REACTIVE_POWER, + state_class=SensorStateClass.MEASUREMENT, + wrapper_class=( + ElectricityReactivePowerRawWrapper, + ElectricityReactivePowerJsonWrapper, + ), + ), + TuyaSensorEntityDescription( + key=f"{dpcode}apparentpower", + dpcode=dpcode, + translation_key=f"{dpcode}_apparent_power", + device_class=SensorDeviceClass.APPARENT_POWER, + state_class=SensorStateClass.MEASUREMENT, + wrapper_class=( + ElectricityApparentPowerRawWrapper, + ElectricityApparentPowerJsonWrapper, + ), + ), + TuyaSensorEntityDescription( + key=f"{dpcode}powerfactor", + dpcode=dpcode, + translation_key=f"{dpcode}_power_factor", + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + wrapper_class=( + ElectricityPowerFactorRawWrapper, + ElectricityPowerFactorJsonWrapper, + ), + ), + ) + + # Commonly used battery sensors, that are reused in the sensors down below. BATTERY_SENSORS: tuple[TuyaSensorEntityDescription, ...] = ( TuyaSensorEntityDescription( @@ -538,78 +606,9 @@ class TuyaSensorEntityDescription(SensorEntityDescription): entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_A}electriccurrent", - dpcode=DPCode.PHASE_A, - translation_key="phase_a_current", - device_class=SensorDeviceClass.CURRENT, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=CURRENT_WRAPPER, - ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_A}power", - dpcode=DPCode.PHASE_A, - translation_key="phase_a_power", - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=POWER_WRAPPER, - ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_A}voltage", - dpcode=DPCode.PHASE_A, - translation_key="phase_a_voltage", - device_class=SensorDeviceClass.VOLTAGE, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=VOLTAGE_WRAPPER, - ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_B}electriccurrent", - dpcode=DPCode.PHASE_B, - translation_key="phase_b_current", - device_class=SensorDeviceClass.CURRENT, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=CURRENT_WRAPPER, - ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_B}power", - dpcode=DPCode.PHASE_B, - translation_key="phase_b_power", - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=POWER_WRAPPER, - ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_B}voltage", - dpcode=DPCode.PHASE_B, - translation_key="phase_b_voltage", - device_class=SensorDeviceClass.VOLTAGE, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=VOLTAGE_WRAPPER, - ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_C}electriccurrent", - dpcode=DPCode.PHASE_C, - translation_key="phase_c_current", - device_class=SensorDeviceClass.CURRENT, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=CURRENT_WRAPPER, - ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_C}power", - dpcode=DPCode.PHASE_C, - translation_key="phase_c_power", - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=POWER_WRAPPER, - ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_C}voltage", - dpcode=DPCode.PHASE_C, - translation_key="phase_c_voltage", - device_class=SensorDeviceClass.VOLTAGE, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=VOLTAGE_WRAPPER, - ), + *_electricity_data(DPCode.PHASE_A), + *_electricity_data(DPCode.PHASE_B), + *_electricity_data(DPCode.PHASE_C), TuyaSensorEntityDescription( key=DPCode.CUR_CURRENT, translation_key="current", @@ -1685,78 +1684,9 @@ class TuyaSensorEntityDescription(SensorEntityDescription): entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_A}electriccurrent", - dpcode=DPCode.PHASE_A, - translation_key="phase_a_current", - device_class=SensorDeviceClass.CURRENT, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=CURRENT_WRAPPER, - ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_A}power", - dpcode=DPCode.PHASE_A, - translation_key="phase_a_power", - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=POWER_WRAPPER, - ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_A}voltage", - dpcode=DPCode.PHASE_A, - translation_key="phase_a_voltage", - device_class=SensorDeviceClass.VOLTAGE, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=VOLTAGE_WRAPPER, - ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_B}electriccurrent", - dpcode=DPCode.PHASE_B, - translation_key="phase_b_current", - device_class=SensorDeviceClass.CURRENT, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=CURRENT_WRAPPER, - ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_B}power", - dpcode=DPCode.PHASE_B, - translation_key="phase_b_power", - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=POWER_WRAPPER, - ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_B}voltage", - dpcode=DPCode.PHASE_B, - translation_key="phase_b_voltage", - device_class=SensorDeviceClass.VOLTAGE, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=VOLTAGE_WRAPPER, - ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_C}electriccurrent", - dpcode=DPCode.PHASE_C, - translation_key="phase_c_current", - device_class=SensorDeviceClass.CURRENT, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=CURRENT_WRAPPER, - ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_C}power", - dpcode=DPCode.PHASE_C, - translation_key="phase_c_power", - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=POWER_WRAPPER, - ), - TuyaSensorEntityDescription( - key=f"{DPCode.PHASE_C}voltage", - dpcode=DPCode.PHASE_C, - translation_key="phase_c_voltage", - device_class=SensorDeviceClass.VOLTAGE, - state_class=SensorStateClass.MEASUREMENT, - wrapper_class=VOLTAGE_WRAPPER, - ), + *_electricity_data(DPCode.PHASE_A), + *_electricity_data(DPCode.PHASE_B), + *_electricity_data(DPCode.PHASE_C), ), DeviceCategory.ZNJDQ: ( TuyaSensorEntityDescription( diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index cd489fcbbde89..bc93406bc4e70 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -891,30 +891,57 @@ "oxydo_reduction_potential": { "name": "Oxydo reduction potential" }, + "phase_a_apparent_power": { + "name": "Phase A apparent power" + }, "phase_a_current": { "name": "Phase A current" }, "phase_a_power": { "name": "Phase A power" }, + "phase_a_power_factor": { + "name": "Phase A power factor" + }, + "phase_a_reactive_power": { + "name": "Phase A reactive power" + }, "phase_a_voltage": { "name": "Phase A voltage" }, + "phase_b_apparent_power": { + "name": "Phase B apparent power" + }, "phase_b_current": { "name": "Phase B current" }, "phase_b_power": { "name": "Phase B power" }, + "phase_b_power_factor": { + "name": "Phase B power factor" + }, + "phase_b_reactive_power": { + "name": "Phase B reactive power" + }, "phase_b_voltage": { "name": "Phase B voltage" }, + "phase_c_apparent_power": { + "name": "Phase C apparent power" + }, "phase_c_current": { "name": "Phase C current" }, "phase_c_power": { "name": "Phase C power" }, + "phase_c_power_factor": { + "name": "Phase C power factor" + }, + "phase_c_reactive_power": { + "name": "Phase C reactive power" + }, "phase_c_voltage": { "name": "Phase C voltage" }, @@ -1058,6 +1085,9 @@ } }, "switch": { + "alarm_push_notifications": { + "name": "Alarm push notifications" + }, "anion": { "name": "Anion" }, @@ -1121,6 +1151,9 @@ "ionizer": { "name": "Ionizer" }, + "keypad_sound": { + "name": "Keypad sound" + }, "motion_alarm": { "name": "Motion alarm" }, diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 4c6e0ec3b4fe9..5d1215f68f55e 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -800,6 +800,18 @@ translation_key="mute", entity_category=EntityCategory.CONFIG, ), + SwitchEntityDescription( + key=DPCode.SWITCH_KB_SOUND, + # Poorly translated as "Key tone switch of host" in the official Tuya app + translation_key="keypad_sound", + entity_category=EntityCategory.CONFIG, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_ALARM_PROPEL, + # Poorly translated as "alarm push switch" in the official Tuya app + translation_key="alarm_push_notifications", + entity_category=EntityCategory.CONFIG, + ), ), DeviceCategory.WK: ( SwitchEntityDescription( diff --git a/homeassistant/components/twilio/__init__.py b/homeassistant/components/twilio/__init__.py index 4c1a94e80b8d0..e4c3d119f6a28 100644 --- a/homeassistant/components/twilio/__init__.py +++ b/homeassistant/components/twilio/__init__.py @@ -11,13 +11,11 @@ from homeassistant.helpers import config_entry_flow, config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DATA_TWILIO, DOMAIN CONF_ACCOUNT_SID = "account_sid" CONF_AUTH_TOKEN = "auth_token" -DATA_TWILIO = DOMAIN - RECEIVED_DATA = f"{DOMAIN}_data_received" CONFIG_SCHEMA = probatio.Schema( @@ -39,8 +37,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True conf = config[DOMAIN] - hass.data[DATA_TWILIO] = Client( - conf.get(CONF_ACCOUNT_SID), conf.get(CONF_AUTH_TOKEN) + hass.data[DATA_TWILIO] = await hass.async_add_executor_job( + Client, conf.get(CONF_ACCOUNT_SID), conf.get(CONF_AUTH_TOKEN) ) return True diff --git a/homeassistant/components/twilio/const.py b/homeassistant/components/twilio/const.py index 7ca44590d6aeb..7492b0aec8ef4 100644 --- a/homeassistant/components/twilio/const.py +++ b/homeassistant/components/twilio/const.py @@ -1,3 +1,12 @@ """Const for Twilio.""" +from typing import TYPE_CHECKING + +from homeassistant.util.hass_dict import HassKey + +if TYPE_CHECKING: + from twilio.rest import Client + DOMAIN = "twilio" + +DATA_TWILIO: HassKey[Client] = HassKey(DOMAIN) diff --git a/homeassistant/components/wake_on_lan/manifest.json b/homeassistant/components/wake_on_lan/manifest.json index c9547419f7983..63c6c90358407 100644 --- a/homeassistant/components/wake_on_lan/manifest.json +++ b/homeassistant/components/wake_on_lan/manifest.json @@ -1,7 +1,7 @@ { "domain": "wake_on_lan", "name": "Wake on LAN", - "codeowners": ["@ntilley905"], + "codeowners": [], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wake_on_lan", "integration_type": "service", diff --git a/homeassistant/components/yalexs_ble/util.py b/homeassistant/components/yalexs_ble/util.py index 15ff53dd1d461..0d5540d20de2d 100644 --- a/homeassistant/components/yalexs_ble/util.py +++ b/homeassistant/components/yalexs_ble/util.py @@ -37,13 +37,24 @@ def async_find_existing_service_info( ) -> BluetoothServiceInfoBleak | None: """Return the service info for the given local_name and address.""" has_unique_local_name = local_name_is_unique(local_name) + by_local_name: BluetoothServiceInfoBleak | None = None + for service_info in async_discovered_service_info(hass): device = service_info.device - if ( - has_unique_local_name and device.name == local_name - ) or device.address == address: + if device.address == address: return service_info - return None + + # The local name is all we have to go on when the address is hidden + # behind a system UUID, but anything can advertise that name, so it + # only counts when nothing answers to the address we were given. + if ( + by_local_name is None + and has_unique_local_name + and device.name == local_name + ): + by_local_name = service_info + + return by_local_name def short_address(address: str) -> str: diff --git a/homeassistant/config.py b/homeassistant/config.py index 96e3a4f7c117b..c2c7ed5e33628 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -479,10 +479,13 @@ def stringify_invalid( message_prefix += f" at {_relpath(hass, annotation[0])}, line {annotation[1]}" path = "->".join(str(m) for m in exc.path) if exc.code == "extra_keys_not_allowed": - return ( - f"{message_prefix}: '{exc.path[-1]}' is an invalid option for '{domain}', " - f"check: {path}{message_suffix}" + message = ( + f"{message_prefix}: '{exc.path[-1]}' is an invalid option for '{domain}'" ) + if candidates := exc.context.get("candidates"): + options = " or ".join(f"'{candidate}'" for candidate in candidates) + message += f" (did you mean {options}?)" + return f"{message}, check: {path}{message_suffix}" if exc.error_message == "required key not provided": return ( f"{message_prefix}: required key '{exc.path[-1]}' not provided" diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index e39a6e505c557..ca8c0453e9034 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -21,7 +21,7 @@ import logging from random import randint from types import MappingProxyType -from typing import TYPE_CHECKING, Any, Self, TypedDict, cast, override +from typing import TYPE_CHECKING, Any, NamedTuple, Self, TypedDict, cast, override from async_interrupt import interrupt import probatio @@ -295,6 +295,20 @@ class OperationNotAllowed(ConfigError): } +class _SetupErrorReason(NamedTuple): + """Error details from a failed config entry setup or migration. + + Field order matches the arguments of `_async_set_state` after `state`, + so an instance can be unpacked directly into it. All fields `None` means + no additional error information is available. + """ + + reason: str | None = None + translation_key: str | None = None + translation_placeholders: dict[str, str] | None = None + translation_domain: str | None = None + + class ConfigFlowContext(FlowContext, total=False): """Typed context dict for config flow.""" @@ -713,6 +727,157 @@ async def async_setup( finally: current_entry.set(None) + @callback + def __async_handle_config_entry_setup_error( + self, + hass: HomeAssistant, + integration: loader.Integration, + exc: BaseException, + *, + migration: bool = False, + ) -> tuple[_SetupErrorReason, bool]: + """Log a config entry setup or migration error and extract its reason. + + For `ConfigEntryNotReady` the state is set to `SETUP_RETRY` and a retry + is scheduled. An actual cancellation is re-raised. + + Returns: + tuple[_SetupErrorReason, bool]: The error reason and whether a retry was scheduled, in which + case the caller must return without setting a state. + """ + logger = self.logger + + reason = _SetupErrorReason() + retry_later = False + + if isinstance(exc, ConfigEntryError): + reason = _SetupErrorReason( + reason=str(exc) or "Unknown fatal config entry error", + translation_key=exc.translation_key, + translation_placeholders=exc.translation_placeholders, + translation_domain=exc.translation_domain, + ) + logger.exception( + "Error migrating entry %s for %s: %s" + if migration + else "Error setting up entry %s for %s: %s", + self.title, + self.domain, + reason.reason, + ) + + elif isinstance(exc, ConfigEntryAuthFailed): + message = str(exc) + auth_base_message = "could not authenticate" + auth_message = ( + f"{auth_base_message}: {message}" if message else auth_base_message + ) + reason = _SetupErrorReason( + reason=message or auth_base_message, + translation_key=exc.translation_key, + translation_placeholders=exc.translation_placeholders, + translation_domain=exc.translation_domain, + ) + logger.warning( + "Config entry '%s' for %s integration %s", + self.title, + self.domain, + auth_message, + ) + logger.debug("Full exception", exc_info=True) # noqa: LOG014 + if not migration: + # Reauth flows assume post-migration config entry state + self.async_start_reauth_if_available(hass) + + elif isinstance(exc, ConfigEntryNotReady): + reason = _SetupErrorReason( + reason=str(exc), + translation_key=exc.translation_key, + translation_placeholders=exc.translation_placeholders, + translation_domain=exc.translation_domain, + ) + retry_later = True + self._async_set_state( + hass, + ConfigEntryState.SETUP_RETRY, + reason.reason or None, + reason.translation_key, + reason.translation_placeholders, + reason.translation_domain, + ) + wait_time = min(2**self._tries * 5, SETUP_RETRY_MAX_WAIT) + ( + randint(RANDOM_MICROSECOND_MIN, RANDOM_MICROSECOND_MAX) / 1000000 + ) + self._tries += 1 + ready_message = ( + f"ready yet: {msg}" if (msg := reason.reason) else "ready yet" + ) + logger.info( + "Config entry migration '%s' for %s integration not %s; Retrying in %d seconds" + if migration + else "Config entry '%s' for %s integration not %s; Retrying in %d seconds", + self.title, + self.domain, + ready_message, + wait_time, + ) + logger.debug("Full exception", exc_info=True) # noqa: LOG014 + + if hass.state is CoreState.running: + self._async_cancel_retry_setup = async_call_later( + hass, + wait_time, + HassJob( + functools.partial(self._async_setup_again, hass), + job_type=HassJobType.Callback, + cancel_on_shutdown=True, + ), + ) + else: + self._async_cancel_retry_setup = hass.bus.async_listen( + EVENT_HOMEASSISTANT_STARTED, + functools.partial(self._async_setup_again, hass), + ) + + elif isinstance(exc, asyncio.CancelledError): + # We want to propagate CancelledError if we are being cancelled. + if (task := asyncio.current_task()) and task.cancelling() > 0: + logger.exception( + "Migration of config entry '%s' for %s integration cancelled" + if migration + else "Setup of config entry '%s' for %s integration cancelled", + self.title, + self.domain, + ) + self._async_set_state( + hass, + ConfigEntryState.SETUP_ERROR, + None, + None, + None, + None, + ) + raise exc + + # This was not a "real" cancellation, log it and treat as a normal error. + logger.exception( + "Error migrating entry %s for %s" + if migration + else "Error setting up entry %s for %s", + self.title, + integration.domain, + ) + + else: + logger.exception( + "Error migrating entry %s for %s" + if migration + else "Error setting up entry %s for %s", + self.title, + integration.domain, + ) + return (reason, retry_later) + async def __async_setup_with_context( self, hass: HomeAssistant, @@ -767,6 +932,10 @@ async def __async_setup_with_context( ) return + reason = _SetupErrorReason() + + result = False + if domain_is_integration: try: await integration.async_get_platform("config_flow") @@ -786,20 +955,28 @@ async def __async_setup_with_context( return # Perform migration - if not await self.async_migrate(hass): - self._async_set_state(hass, ConfigEntryState.MIGRATION_ERROR, None) + try: + migration_result = await self.async_migrate(hass) + except ( + asyncio.CancelledError, + SystemExit, + Exception, # noqa: BLE001 + ) as exc: + reason, retry_later = self.__async_handle_config_entry_setup_error( + hass, integration, exc, migration=True + ) + if retry_later: + return + migration_result = False + + if not migration_result: + self._async_set_state(hass, ConfigEntryState.MIGRATION_ERROR, *reason) return setup_phase = SetupPhases.CONFIG_ENTRY_SETUP else: setup_phase = SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP - error_reason = None - error_reason_translation_domain = None - error_reason_translation_key = None - error_reason_translation_placeholders = None - - result = False try: with async_start_setup( hass, integration=self.domain, group=self.entry_id, phase=setup_phase @@ -811,107 +988,16 @@ async def __async_setup_with_context( "%s.async_setup_entry did not return boolean", integration.domain ) result = False - except ConfigEntryError as exc: - error_reason = str(exc) or "Unknown fatal config entry error" - error_reason_translation_domain = exc.translation_domain - error_reason_translation_key = exc.translation_key - error_reason_translation_placeholders = exc.translation_placeholders - logger.exception( - "Error setting up entry %s for %s: %s", - self.title, - self.domain, - error_reason, - ) - except ConfigEntryAuthFailed as exc: - message = str(exc) - auth_base_message = "could not authenticate" - error_reason = message or auth_base_message - error_reason_translation_domain = exc.translation_domain - error_reason_translation_key = exc.translation_key - error_reason_translation_placeholders = exc.translation_placeholders - auth_message = ( - f"{auth_base_message}: {message}" if message else auth_base_message - ) - logger.warning( - "Config entry '%s' for %s integration %s", - self.title, - self.domain, - auth_message, - ) - logger.debug("Full exception", exc_info=True) - self.async_start_reauth_if_available(hass) - except ConfigEntryNotReady as exc: - message = str(exc) - error_reason_translation_domain = exc.translation_domain - error_reason_translation_key = exc.translation_key - error_reason_translation_placeholders = exc.translation_placeholders - self._async_set_state( - hass, - ConfigEntryState.SETUP_RETRY, - message or None, - error_reason_translation_key, - error_reason_translation_placeholders, - error_reason_translation_domain, - ) - wait_time = min(2**self._tries * 5, SETUP_RETRY_MAX_WAIT) + ( - randint(RANDOM_MICROSECOND_MIN, RANDOM_MICROSECOND_MAX) / 1000000 - ) - self._tries += 1 - ready_message = f"ready yet: {message}" if message else "ready yet" - logger.info( - "Config entry '%s' for %s integration not %s; Retrying in %d seconds", - self.title, - self.domain, - ready_message, - wait_time, - ) - logger.debug("Full exception", exc_info=True) - - if hass.state is CoreState.running: - self._async_cancel_retry_setup = async_call_later( - hass, - wait_time, - HassJob( - functools.partial(self._async_setup_again, hass), - job_type=HassJobType.Callback, - cancel_on_shutdown=True, - ), - ) - else: - self._async_cancel_retry_setup = hass.bus.async_listen( - EVENT_HOMEASSISTANT_STARTED, - functools.partial(self._async_setup_again, hass), - ) - - return - - except asyncio.CancelledError: - # We want to propagate CancelledError if we are being cancelled. - if (task := asyncio.current_task()) and task.cancelling() > 0: - logger.exception( - "Setup of config entry '%s' for %s integration cancelled", - self.title, - self.domain, - ) - self._async_set_state( - hass, - ConfigEntryState.SETUP_ERROR, - None, - None, - None, - ) - raise - - # This was not a "real" cancellation, log it and treat as a normal error. - logger.exception( - "Error setting up entry %s for %s", self.title, integration.domain - ) - - # pylint: disable-next=broad-except - except SystemExit, Exception: - logger.exception( - "Error setting up entry %s for %s", self.title, integration.domain + except ( + asyncio.CancelledError, + SystemExit, + Exception, # noqa: BLE001 + ) as exc: + reason, retry_later = self.__async_handle_config_entry_setup_error( + hass, integration, exc ) + if retry_later: + return finally: if not result and domain_is_integration: @@ -936,14 +1022,7 @@ async def __async_setup_with_context( if result: self._async_set_state(hass, ConfigEntryState.LOADED, None) else: - self._async_set_state( - hass, - ConfigEntryState.SETUP_ERROR, - error_reason, - error_reason_translation_key, - error_reason_translation_placeholders, - error_reason_translation_domain, - ) + self._async_set_state(hass, ConfigEntryState.SETUP_ERROR, *reason) @callback def _async_setup_again(self, hass: HomeAssistant, *_: Any) -> None: @@ -1154,7 +1233,12 @@ def _async_set_state( async def async_migrate(self, hass: HomeAssistant) -> bool: """Migrate an entry. - Returns True if config entry is up-to-date or has been migrated. + Returns: + True: if the config entry is up-to-date or has been migrated. + False: otherwise. + + Raises: + Exception: Driven by the integration being migrated. """ if (handler := HANDLERS.get(self.domain)) is None: self.logger.error( @@ -1196,20 +1280,15 @@ async def async_migrate(self, hass: HomeAssistant) -> bool: ) return False - try: - result = await component.async_migrate_entry(hass, self) - if not isinstance(result, bool): - self.logger.error( # type: ignore[unreachable] - "%s.async_migrate_entry did not return boolean", self.domain - ) - return False - if result: - hass.config_entries._async_schedule_save() # noqa: SLF001 - except Exception: - self.logger.exception( - "Error migrating entry %s for %s", self.title, self.domain + result = await component.async_migrate_entry(hass, self) + if not isinstance(result, bool): + self.logger.error( # type: ignore[unreachable] + "%s.async_migrate_entry did not return boolean", self.domain ) return False + if result: + hass.config_entries._async_schedule_save() # noqa: SLF001 + return result def add_update_listener(self, listener: UpdateListenerType) -> CALLBACK_TYPE: diff --git a/requirements_all.txt b/requirements_all.txt index c7d42576de91a..97aee4a066a35 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -667,7 +667,7 @@ batinfo==0.4.2 beautifulsoup4==4.13.3 # homeassistant.components.besen -besen==0.4.0 +besen==0.4.2 # homeassistant.components.bitvis bitvis-protobuf==2.0.4 @@ -1623,7 +1623,7 @@ micloud==0.5 microBeesPy==0.3.5 # homeassistant.components.midea -midea-local==11.0.1 +midea-local==12.0.0 # homeassistant.components.mill mill-local==0.5.0 @@ -1641,7 +1641,7 @@ mitsubishi-comfort==0.5.2 moat-ble==0.1.1 # homeassistant.components.modbus -modbus-connection[tmodbus]==4.11.1 +modbus-connection[tmodbus]==4.12.0 # homeassistant.components.moehlenhoff_alpha2 moehlenhoff-alpha2==1.4.0 @@ -3296,7 +3296,7 @@ tonewinner-rs232==1.1.0 toonapi==0.3.0 # homeassistant.components.totalconnect -total-connect-client==2026.7 +total-connect-client==2026.9 # homeassistant.components.tplink_omada tplink-omada-client==1.5.10 diff --git a/tests/components/midea/snapshots/test_binary_sensor.ambr b/tests/components/midea/snapshots/test_binary_sensor.ambr index 93307d250fab5..5d4b49329e923 100644 --- a/tests/components/midea/snapshots/test_binary_sensor.ambr +++ b/tests/components/midea/snapshots/test_binary_sensor.ambr @@ -50,6 +50,57 @@ 'state': 'on', }) # --- +# name: test_all_entities[ac][binary_sensor.air_conditioner_water_pump_running-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.air_conditioner_water_pump_running', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Water pump running', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Water pump running', + 'platform': 'midea', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'water_pump_running', + 'unique_id': '12345678_water_pump_running', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[ac][binary_sensor.air_conditioner_water_pump_running-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'running', + : 'Air Conditioner Water pump running', + }), + 'context': , + 'entity_id': 'binary_sensor.air_conditioner_water_pump_running', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_all_entities[e1][binary_sensor.dishwasher_opening-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/midea/test_binary_sensor.py b/tests/components/midea/test_binary_sensor.py index f3a9c27f6d40e..62cd1b5e26860 100644 --- a/tests/components/midea/test_binary_sensor.py +++ b/tests/components/midea/test_binary_sensor.py @@ -39,6 +39,7 @@ def _ac_device() -> DummyDevice: ACAttributes.swing_horizontal: True, ACAttributes.indoor_humidity: 50, ACAttributes.full_dust: True, + ACAttributes.water_pump_running: True, }, ) diff --git a/tests/components/modbus/test_connection.py b/tests/components/modbus/test_connection.py index fb95d1cc18648..d89649a4b6e91 100644 --- a/tests/components/modbus/test_connection.py +++ b/tests/components/modbus/test_connection.py @@ -3,7 +3,7 @@ from collections.abc import Callable, Generator from unittest.mock import AsyncMock, patch -from modbus_connection import ModbusSerialParams, ModbusTcpParams +from modbus_connection import ModbusSerialParams, ModbusTcpParams, ModbusTlsParams from modbus_connection.tmodbus import ModbusConnection import pytest @@ -100,6 +100,63 @@ async def test_the_same_device_reached_by_a_different_name_still_shares( assert len(hass.data[DATA_MODBUS_CONNECTIONS]) == 1 +@pytest.mark.parametrize( + "device", + [ + pytest.param("socket://1.2.3.4:502", id="ipv4"), + pytest.param("socket://[fe80::1]:502", id="ipv6"), + ], +) +async def test_a_serial_framing_over_a_socket_shares_the_serial_link( + hass: HomeAssistant, consumer: ConsumerFactory, device: str +) -> None: + """A serial framing on a TCP link is the serial link the socket device names. + + Both spellings build one link at one line speed, so splitting them would put + a second connection on a gateway that can only answer one. + """ + entry = consumer() + await hass.config_entries.async_setup(entry.entry_id) + host = device.removeprefix("socket://").rpartition(":")[0].strip("[]") + + async_get_unit( + hass, entry, ModbusSerialParams(device=device, framer="rtu", baudrate=115200), 1 + ) + with pytest.deprecated_call(): + async_get_unit( + hass, entry, ModbusTcpParams(host=host, port=502, framer="rtu"), 2 + ) + + assert len(hass.data[DATA_MODBUS_CONNECTIONS]) == 1 + + +async def test_a_serial_framing_over_a_socket_keeps_the_line_speed_asked_for( + hass: HomeAssistant, consumer: ConsumerFactory +) -> None: + """The framing alone says nothing about the line speed, so it cannot assume one. + + A slower line needs a longer gap between frames, which the connection built + for the faster one does not leave. + """ + entry = consumer() + await hass.config_entries.async_setup(entry.entry_id) + + async_get_unit( + hass, + entry, + ModbusSerialParams(device="socket://1.2.3.4:502", framer="rtu", baudrate=9600), + 1, + ) + + with ( + pytest.deprecated_call(), + pytest.raises(HomeAssistantError, match="different link settings"), + ): + async_get_unit( + hass, entry, ModbusTcpParams(host="1.2.3.4", port=502, framer="rtu"), 2 + ) + + async def test_one_device_cannot_be_used_with_two_link_settings( hass: HomeAssistant, consumer: ConsumerFactory ) -> None: @@ -114,9 +171,7 @@ async def test_one_device_cannot_be_used_with_two_link_settings( async_get_unit(hass, entry, ModbusTcpParams(host="1.2.3.4", port=502), 1) with pytest.raises(HomeAssistantError, match="different link settings"): - async_get_unit( - hass, entry, ModbusTcpParams(host="1.2.3.4", port=502, framer="rtu"), 2 - ) + async_get_unit(hass, entry, ModbusTlsParams(host="1.2.3.4", port=502), 2) async def test_the_last_consumer_closes_the_connection( @@ -251,7 +306,7 @@ async def test_a_temporary_unit_cannot_clash_with_held_link_settings( with pytest.raises(HomeAssistantError, match="different link settings"): async with async_get_temporary_unit( - hass, ModbusTcpParams(host="1.2.3.4", port=502, framer="rtu"), 2 + hass, ModbusTlsParams(host="1.2.3.4", port=502), 2 ): pass diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index f0190c69773c1..5493d93a44f4b 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -37,7 +37,12 @@ STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.exceptions import ( + HomeAssistantError, + ServiceValidationError, + Unauthorized, + UnknownUser, +) from homeassistant.helpers import ( device_registry as dr, entity_registry as er, @@ -56,6 +61,7 @@ MockEntity, MockEntityPlatform, MockMqttReasonCode, + MockUser, async_capture_events, async_fire_mqtt_message, async_fire_time_changed, @@ -1113,6 +1119,77 @@ async def test_dump_service( assert writes[0][1][0] == ["bla/1,test1\n", "bla/2,test2\n"] +ADMIN_SERVICE_CALLS = [ + pytest.param( + mqtt.SERVICE_PUBLISH, + {mqtt.ATTR_TOPIC: "test/topic", mqtt.ATTR_PAYLOAD: "payload"}, + id="publish", + ), + pytest.param(mqtt.SERVICE_DUMP, {"topic": "bla/#", "duration": 3}, id="dump"), +] + + +@pytest.mark.parametrize(("service", "service_data"), ADMIN_SERVICE_CALLS) +async def test_admin_service_as_admin( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + hass_admin_user: MockUser, + service: str, + service_data: dict[str, Any], +) -> None: + """Test an admin user can call the action.""" + await mqtt_mock_entry() + + await hass.services.async_call( + mqtt.DOMAIN, + service, + service_data, + blocking=True, + context=ha.Context(user_id=hass_admin_user.id), + ) + + +@pytest.mark.parametrize(("service", "service_data"), ADMIN_SERVICE_CALLS) +async def test_admin_service_as_non_admin( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + hass_read_only_user: MockUser, + service: str, + service_data: dict[str, Any], +) -> None: + """Test a non-admin user cannot call the action.""" + await mqtt_mock_entry() + + with pytest.raises(Unauthorized): + await hass.services.async_call( + mqtt.DOMAIN, + service, + service_data, + blocking=True, + context=ha.Context(user_id=hass_read_only_user.id), + ) + + +@pytest.mark.parametrize(("service", "service_data"), ADMIN_SERVICE_CALLS) +async def test_admin_service_as_unknown_user( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + service: str, + service_data: dict[str, Any], +) -> None: + """Test a user that no longer exists cannot call the action.""" + await mqtt_mock_entry() + + with pytest.raises(UnknownUser): + await hass.services.async_call( + mqtt.DOMAIN, + service, + service_data, + blocking=True, + context=ha.Context(user_id="i-am-not-a-user"), + ) + + async def test_mqtt_ws_remove_discovered_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/components/samsungtv/fixtures/device_info_UN55KU6290.json b/tests/components/samsungtv/fixtures/device_info_UN55KU6290.json new file mode 100644 index 0000000000000..f84a97ce14c2e --- /dev/null +++ b/tests/components/samsungtv/fixtures/device_info_UN55KU6290.json @@ -0,0 +1,28 @@ +{ + "id": "uuid:0dd7f9c9-b9a0-4b7e-8c3e-1f2b3c4d5e6f", + "name": "[TV] Samsung 6 Series (55)", + "version": "2.1.0", + "device": { + "type": "Samsung SmartTV", + "duid": "uuid:0dd7f9c9-b9a0-4b7e-8c3e-1f2b3c4d5e6f", + "model": "16_JAZZL_UHD_BASIC", + "modelName": "UN55KU6290", + "description": "Samsung DTV RCR", + "networkType": "wired", + "ssid": "", + "ip": "1.2.3.4", + "firmwareVersion": "Unknown", + "name": "[TV] Samsung 6 Series (55)", + "id": "uuid:0dd7f9c9-b9a0-4b7e-8c3e-1f2b3c4d5e6f", + "udn": "uuid:0dd7f9c9-b9a0-4b7e-8c3e-1f2b3c4d5e6f", + "resolution": "3840x2160", + "countryCode": "US", + "msfVersion": "2.1.0", + "smartHubAgreement": "true", + "wifiMac": "aa:bb:aa:aa:aa:aa", + "developerMode": "0", + "developerIP": "" + }, + "type": "Samsung SmartTV", + "uri": "https://1.2.3.4:8002/api/v2/" +} diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 4c78e7e2762e6..c1a3eba8736e0 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -30,8 +30,11 @@ CONF_SSDP_RENDERING_CONTROL_LOCATION, DEFAULT_MANUFACTURER, DOMAIN, + ENCRYPTED_WEBSOCKET_PORT, LEGACY_PORT, + METHOD_ENCRYPTED_WEBSOCKET, METHOD_LEGACY, + METHOD_WEBSOCKET, RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT, RESULT_NOT_SUPPORTED, @@ -283,6 +286,153 @@ async def test_user_encrypted_websocket( assert result4["result"].unique_id == "223da676-497a-4e06-9507-5e27ec4f0fb3" +@pytest.mark.usefixtures("rest_api", "remote_encrypted_websocket") +async def test_user_websocket_k_series_encrypted_fallback( + hass: HomeAssistant, rest_api: Mock +) -> None: + """Test a 2016 K-series set falls back to encrypted pairing (#177252). + + Its REST device info selects the websocket method, but the token handshake + times out (RESULT_CANNOT_CONNECT) because it only pairs via the encrypted + CloudPINPage flow. The encrypted port is reachable, so the flow probes it + successfully before committing to the encrypted pairing step. + """ + rest_api.rest_device_info.return_value = await async_load_json_object_fixture( + hass, "device_info_UN55KU6290.json", DOMAIN + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + with ( + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + side_effect=OSError("timed out"), + ), + patch( + "homeassistant.components.samsungtv.config_flow.SamsungTVEncryptedWSAsyncAuthenticator", + autospec=True, + ) as authenticator_mock, + ): + authenticator_mock.return_value.try_pin.side_effect = [ + None, + "037739871315caef138547b03e348b72", + ] + authenticator_mock.return_value.get_session_id_and_close.return_value = "1" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "encrypted_pairing" + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], user_input={CONF_PIN: "invalid"} + ) + assert result3["step_id"] == "encrypted_pairing" + assert result3["errors"] == {"base": "invalid_pin"} + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], user_input={CONF_PIN: "1234"} + ) + + assert result4["type"] is FlowResultType.CREATE_ENTRY + assert result4["data"][CONF_METHOD] == METHOD_ENCRYPTED_WEBSOCKET + assert result4["data"][CONF_MODEL] == "UN55KU6290" + assert result4["data"][CONF_PORT] == ENCRYPTED_WEBSOCKET_PORT + assert result4["data"][CONF_TOKEN] == "037739871315caef138547b03e348b72" + assert result4["data"][CONF_SESSION_ID] == "1" + + +@pytest.mark.usefixtures("remote_websocket", "rest_api") +async def test_user_websocket_k_series_stays_on_websocket( + hass: HomeAssistant, rest_api: Mock +) -> None: + """Test a K-series set that pairs over websocket is not pushed to encrypted. + + Regression guard for #70708 (UE32K5600): the encrypted fallback must only + trigger when the websocket pairing genuinely fails to connect. + """ + rest_api.rest_device_info.return_value = await async_load_json_object_fixture( + hass, "device_info_UN55KU6290.json", DOMAIN + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["data"][CONF_METHOD] == METHOD_WEBSOCKET + assert result2["data"][CONF_MODEL] == "UN55KU6290" + assert result2["data"][CONF_PORT] == 8002 + + +@pytest.mark.usefixtures("rest_api") +async def test_user_websocket_non_k_series_cannot_connect( + hass: HomeAssistant, rest_api: Mock +) -> None: + """Test a non-K-series set that fails to connect is not pushed to encrypted. + + Regression guard for #70708: only K-series models get the encrypted + fallback, so a websocket connection failure on any other model must abort + with cannot_connect. + """ + rest_api.rest_device_info.return_value = await async_load_json_object_fixture( + hass, "device_info_UE43LS003.json", DOMAIN + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + side_effect=OSError("timed out"), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == RESULT_CANNOT_CONNECT + + +@pytest.mark.usefixtures("rest_api", "remote_encrypted_websocket_failing") +async def test_user_websocket_k_series_encrypted_also_cannot_connect( + hass: HomeAssistant, rest_api: Mock +) -> None: + """Test a K-series set that is offline aborts cleanly with cannot_connect. + + Websocket pairing fails to connect, and the encrypted port is probed and + also unreachable (e.g. the TV is off), so the flow must abort with + cannot_connect instead of raising out of the encrypted pairing step. + """ + rest_api.rest_device_info.return_value = await async_load_json_object_fixture( + hass, "device_info_UN55KU6290.json", DOMAIN + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + side_effect=OSError("timed out"), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == RESULT_CANNOT_CONNECT + + @pytest.mark.usefixtures("rest_api_failing") async def test_user_legacy_missing_auth(hass: HomeAssistant) -> None: """Test starting a flow by user with authentication.""" diff --git a/tests/components/stiebel_eltron/test_config_flow.py b/tests/components/stiebel_eltron/test_config_flow.py index 5653da2c0ff97..df3d748cec12a 100644 --- a/tests/components/stiebel_eltron/test_config_flow.py +++ b/tests/components/stiebel_eltron/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from modbus_connection import ModbusTcpParams +from modbus_connection import ModbusTlsParams from pystiebeleltron import ControllerModel, StiebelEltronModbusError import pytest @@ -79,9 +79,7 @@ async def test_form_conflicting_link_settings(hass: HomeAssistant) -> None: async_get_unit( hass, other_entry, - ModbusTcpParams( - host=USER_INPUT[CONF_HOST], port=USER_INPUT[CONF_PORT], framer="rtu" - ), + ModbusTlsParams(host=USER_INPUT[CONF_HOST], port=USER_INPUT[CONF_PORT]), UNIT_ID, ) diff --git a/tests/components/stiebel_eltron/test_init.py b/tests/components/stiebel_eltron/test_init.py index dec8f65e7b31f..fb6a4cb2e13c4 100644 --- a/tests/components/stiebel_eltron/test_init.py +++ b/tests/components/stiebel_eltron/test_init.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch from freezegun.api import FrozenDateTimeFactory -from modbus_connection import ModbusError, ModbusTcpParams +from modbus_connection import ModbusError, ModbusTcpParams, ModbusTlsParams from modbus_connection.mock import MockModbusConnection from pystiebeleltron import StiebelEltronModbusError import pytest @@ -82,7 +82,7 @@ async def test_async_setup_entry_conflicting_link_settings( async_get_unit( hass, other_entry, - ModbusTcpParams(host="1.1.1.1", port=502, framer="rtu"), + ModbusTlsParams(host="1.1.1.1", port=502), UNIT_ID, ) mock_config_entry.add_to_hass(hass) diff --git a/tests/components/teslemetry/test_config_flow.py b/tests/components/teslemetry/test_config_flow.py index 12bb4749926eb..493a3b7f884a3 100644 --- a/tests/components/teslemetry/test_config_flow.py +++ b/tests/components/teslemetry/test_config_flow.py @@ -796,6 +796,7 @@ async def _start_pairing_at_scan( return result +@pytest.mark.usefixtures("enable_bluetooth") async def test_subentry_pairing_already_whitelisted(hass: HomeAssistant) -> None: """The add flow creates the subentry when the key is already whitelisted.""" entry = await _setup_account_entry(hass) @@ -828,6 +829,7 @@ async def test_subentry_pairing_already_whitelisted(hass: HomeAssistant) -> None vehicle.disconnect.assert_awaited_once() +@pytest.mark.usefixtures("enable_bluetooth") async def test_subentry_pairing_duplicate_vin_aborts(hass: HomeAssistant) -> None: """A second flow racing on the same VIN aborts with already_configured.""" entry = await _setup_account_entry(hass) @@ -866,6 +868,7 @@ async def test_subentry_pairing_duplicate_vin_aborts(hass: HomeAssistant) -> Non assert len(entry.get_subentries_of_type(SUBENTRY_TYPE_VEHICLE)) == 1 +@pytest.mark.usefixtures("enable_bluetooth") async def test_subentry_pairing_requires_key_approval(hass: HomeAssistant) -> None: """Pairing walks through instructions and key install when not whitelisted.""" entry = await _setup_account_entry(hass) @@ -913,6 +916,7 @@ async def _pair() -> None: vehicle.pair.assert_awaited_once() +@pytest.mark.usefixtures("enable_bluetooth") async def test_subentry_scan_connect_fails(hass: HomeAssistant) -> None: """The scan step re-shows the form with an error when BLE connect fails.""" entry = await _setup_account_entry(hass) @@ -951,6 +955,7 @@ async def test_subentry_scan_connect_fails(hass: HomeAssistant) -> None: ], ids=["timeout", "transport", "rejected"], ) +@pytest.mark.usefixtures("enable_bluetooth") async def test_subentry_authorize_failure( hass: HomeAssistant, error: type[TeslaFleetError], expected: str ) -> None: @@ -998,6 +1003,7 @@ async def _pair() -> None: vehicle.pair.assert_awaited_once() +@pytest.mark.usefixtures("enable_bluetooth") async def test_subentry_authorize_unexpected_error_disconnects( hass: HomeAssistant, ) -> None: @@ -1029,6 +1035,7 @@ async def test_subentry_authorize_unexpected_error_disconnects( assert not entry.get_subentries_of_type(SUBENTRY_TYPE_VEHICLE) +@pytest.mark.usefixtures("enable_bluetooth") async def test_subentry_authorize_existing_key_finishes(hass: HomeAssistant) -> None: """Approving the key after a timeout, then retrying, completes the pairing.""" entry = await _setup_account_entry(hass) @@ -1103,6 +1110,7 @@ async def _pair() -> None: pytest.param(TimeoutError(), id="timeout_error"), ], ) +@pytest.mark.usefixtures("enable_bluetooth") async def test_subentry_handshake_error_recovers( hass: HomeAssistant, handshake_error: Exception ) -> None: @@ -1145,6 +1153,7 @@ async def test_subentry_handshake_error_recovers( assert vehicle.disconnect.await_count == 2 +@pytest.mark.usefixtures("enable_bluetooth") async def test_subentry_pairing_abandoned(hass: HomeAssistant) -> None: """Abandoning the flow mid-pairing cancels the pair task and disconnects.""" entry = await _setup_account_entry(hass) @@ -1188,6 +1197,7 @@ async def _pair() -> None: assert not entry.get_subentries_of_type(SUBENTRY_TYPE_VEHICLE) +@pytest.mark.usefixtures("enable_bluetooth") async def test_subentry_scan_device_not_found(hass: HomeAssistant) -> None: """The scan step re-shows the form with an error when no device is found.""" entry = await _setup_account_entry(hass) @@ -1225,6 +1235,7 @@ async def test_subentry_scan_device_not_found(hass: HomeAssistant) -> None: ), ], ) +@pytest.mark.usefixtures("enable_bluetooth") async def test_subentry_scan_key_load_recovers( hass: HomeAssistant, key_error: Exception ) -> None: @@ -1266,6 +1277,7 @@ async def test_subentry_scan_key_load_recovers( vehicle.connect.assert_awaited_once() +@pytest.mark.usefixtures("enable_bluetooth") async def test_subentry_scan_finds_device_after_active_scan( hass: HomeAssistant, ) -> None: @@ -1306,6 +1318,7 @@ async def _active_scan(hass: HomeAssistant) -> None: vehicle.connect.assert_awaited_once() +@pytest.mark.usefixtures("enable_bluetooth") async def test_subentry_add_flow_keeps_device_on_parent( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -1408,6 +1421,7 @@ async def test_subentry_add_flow_keeps_device_on_parent( assert all(entity.config_subentry_id is None for entity in bound_entities) +@pytest.mark.usefixtures("enable_bluetooth") async def test_subentry_add_flow_no_available_vehicles(hass: HomeAssistant) -> None: """The add flow aborts when every account vehicle is already added.""" entry = await _setup_paired_entry(hass) @@ -1421,6 +1435,20 @@ async def test_subentry_add_flow_no_available_vehicles(hass: HomeAssistant) -> N assert result["reason"] == "no_vehicles" +async def test_subentry_add_flow_no_bluetooth(hass: HomeAssistant) -> None: + """The add flow aborts immediately when no Bluetooth integration is set up.""" + entry = await _setup_account_entry(hass) + + result = await hass.config_entries.subentries.async_init( + (entry.entry_id, SUBENTRY_TYPE_VEHICLE), + context={"source": "user"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "bluetooth_not_available" + + +@pytest.mark.usefixtures("enable_bluetooth") async def test_subentry_add_flow_entry_not_loaded(hass: HomeAssistant) -> None: """The add flow aborts when the parent entry is not loaded.""" entry = mock_config_entry() diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index d354446a8df9d..7f66aca0b9a02 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -6375,6 +6375,67 @@ 'state': '222.4', }) # --- +# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_apparent_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.duan_lu_qi_ha_phase_a_apparent_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Phase A apparent power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kVA', + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A apparent power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_apparent_power', + 'unique_id': 'tuya.qi94v9dmdx4fkpncqldphase_aapparentpower', + 'unit_of_measurement': 'kVA', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_apparent_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'apparent_power', + : '断路器HA Phase A apparent power', + : , + : 'kVA', + }), + 'context': , + 'entity_id': 'sensor.duan_lu_qi_ha_phase_a_apparent_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.008', + }) +# --- # name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_current-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -6497,6 +6558,121 @@ 'state': '0.008', }) # --- +# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_power_factor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.duan_lu_qi_ha_phase_a_power_factor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Phase A power factor', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A power factor', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_power_factor', + 'unique_id': 'tuya.qi94v9dmdx4fkpncqldphase_apowerfactor', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_power_factor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'power_factor', + : '断路器HA Phase A power factor', + : , + }), + 'context': , + 'entity_id': 'sensor.duan_lu_qi_ha_phase_a_power_factor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.99', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_reactive_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.duan_lu_qi_ha_phase_a_reactive_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Phase A reactive power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kvar', + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A reactive power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_reactive_power', + 'unique_id': 'tuya.qi94v9dmdx4fkpncqldphase_areactivepower', + 'unit_of_measurement': 'kvar', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_reactive_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'reactive_power', + : '断路器HA Phase A reactive power', + : , + : 'kvar', + }), + 'context': , + 'entity_id': 'sensor.duan_lu_qi_ha_phase_a_reactive_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -7147,6 +7323,67 @@ 'state': '236.5', }) # --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_apparent_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.edesanya_energy_phase_b_apparent_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Phase B apparent power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kVA', + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase B apparent power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_b_apparent_power', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_bapparentpower', + 'unit_of_measurement': 'kVA', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_apparent_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'apparent_power', + : 'Edesanya Energy Phase B apparent power', + : , + : 'kVA', + }), + 'context': , + 'entity_id': 'sensor.edesanya_energy_phase_b_apparent_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_current-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -7269,7 +7506,7 @@ 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_voltage-entry] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_power_factor-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -7285,7 +7522,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.edesanya_energy_phase_b_voltage', + 'entity_id': 'sensor.edesanya_energy_phase_b_power_factor', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -7293,41 +7530,37 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Phase B voltage', + 'object_id_base': 'Phase B power factor', 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Phase B voltage', + 'original_name': 'Phase B power factor', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'phase_b_voltage', - 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_bvoltage', - 'unit_of_measurement': 'V', + 'translation_key': 'phase_b_power_factor', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_bpowerfactor', + 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_voltage-state] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_power_factor-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - : 'voltage', - : 'Edesanya Energy Phase B voltage', + : 'power_factor', + : 'Edesanya Energy Phase B power factor', : , - : 'V', }), 'context': , - 'entity_id': 'sensor.edesanya_energy_phase_b_voltage', + 'entity_id': 'sensor.edesanya_energy_phase_b_power_factor', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_current-entry] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_reactive_power-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -7343,7 +7576,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.edesanya_energy_phase_c_current', + 'entity_id': 'sensor.edesanya_energy_phase_b_reactive_power', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -7351,44 +7584,44 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Phase C current', + 'object_id_base': 'Phase B reactive power', 'options': dict({ 'sensor': dict({ 'suggested_display_precision': 2, }), 'sensor.private': dict({ - 'suggested_unit_of_measurement': 'A', + 'suggested_unit_of_measurement': 'kvar', }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Phase C current', + 'original_name': 'Phase B reactive power', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'phase_c_current', - 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_celectriccurrent', - 'unit_of_measurement': 'A', + 'translation_key': 'phase_b_reactive_power', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_breactivepower', + 'unit_of_measurement': 'kvar', }) # --- -# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_current-state] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_reactive_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - : 'current', - : 'Edesanya Energy Phase C current', + : 'reactive_power', + : 'Edesanya Energy Phase B reactive power', : , - : 'A', + : 'kvar', }), 'context': , - 'entity_id': 'sensor.edesanya_energy_phase_c_current', + 'entity_id': 'sensor.edesanya_energy_phase_b_reactive_power', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_power-entry] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -7404,7 +7637,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.edesanya_energy_phase_c_power', + 'entity_id': 'sensor.edesanya_energy_phase_b_voltage', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -7412,44 +7645,41 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Phase C power', + 'object_id_base': 'Phase B voltage', 'options': dict({ 'sensor': dict({ - 'suggested_display_precision': 2, - }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': 'kW', + 'suggested_display_precision': 0, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Phase C power', + 'original_name': 'Phase B voltage', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'phase_c_power', - 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_cpower', - 'unit_of_measurement': 'kW', + 'translation_key': 'phase_b_voltage', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_bvoltage', + 'unit_of_measurement': 'V', }) # --- -# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_power-state] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_b_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - : 'power', - : 'Edesanya Energy Phase C power', + : 'voltage', + : 'Edesanya Energy Phase B voltage', : , - : 'kW', + : 'V', }), 'context': , - 'entity_id': 'sensor.edesanya_energy_phase_c_power', + 'entity_id': 'sensor.edesanya_energy_phase_b_voltage', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_voltage-entry] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_apparent_power-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -7465,7 +7695,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.edesanya_energy_phase_c_voltage', + 'entity_id': 'sensor.edesanya_energy_phase_c_apparent_power', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -7473,48 +7703,51 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Phase C voltage', + 'object_id_base': 'Phase C apparent power', 'options': dict({ 'sensor': dict({ - 'suggested_display_precision': 0, + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kVA', }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Phase C voltage', + 'original_name': 'Phase C apparent power', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'phase_c_voltage', - 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_cvoltage', - 'unit_of_measurement': 'V', + 'translation_key': 'phase_c_apparent_power', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_capparentpower', + 'unit_of_measurement': 'kVA', }) # --- -# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_voltage-state] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_apparent_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - : 'voltage', - : 'Edesanya Energy Phase C voltage', + : 'apparent_power', + : 'Edesanya Energy Phase C apparent power', : , - : 'V', + : 'kVA', }), 'context': , - 'entity_id': 'sensor.edesanya_energy_phase_c_voltage', + 'entity_id': 'sensor.edesanya_energy_phase_c_apparent_power', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[sensor.edesanya_energy_total_energy-entry] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_current-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, ]), 'area_id': None, 'capabilities': dict({ - : , + : , }), 'config_entry_id': , 'config_subentry_id': , @@ -7523,7 +7756,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.edesanya_energy_total_energy', + 'entity_id': 'sensor.edesanya_energy_phase_c_current', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -7531,41 +7764,44 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Total energy', + 'object_id_base': 'Phase C current', 'options': dict({ 'sensor': dict({ 'suggested_display_precision': 2, }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'A', + }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Total energy', + 'original_name': 'Phase C current', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'total_energy', - 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldtotal_forward_energy', - 'unit_of_measurement': , + 'translation_key': 'phase_c_current', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_celectriccurrent', + 'unit_of_measurement': 'A', }) # --- -# name: test_platform_setup_and_discovery[sensor.edesanya_energy_total_energy-state] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - : 'energy', - : 'Edesanya Energy Total energy', - : , - : , + : 'current', + : 'Edesanya Energy Phase C current', + : , + : 'A', }), 'context': , - 'entity_id': 'sensor.edesanya_energy_total_energy', + 'entity_id': 'sensor.edesanya_energy_phase_c_current', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '219.72', + 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_current-entry] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_power-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -7581,7 +7817,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.elivco_kitchen_socket_current', + 'entity_id': 'sensor.edesanya_energy_phase_c_power', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -7589,44 +7825,44 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Current', + 'object_id_base': 'Phase C power', 'options': dict({ 'sensor': dict({ 'suggested_display_precision': 2, }), 'sensor.private': dict({ - 'suggested_unit_of_measurement': , + 'suggested_unit_of_measurement': 'kW', }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Current', + 'original_name': 'Phase C power', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'current', - 'unique_id': 'tuya.cq4hzlrnqn4qi0mqzccur_current', - 'unit_of_measurement': , + 'translation_key': 'phase_c_power', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_cpower', + 'unit_of_measurement': 'kW', }) # --- -# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_current-state] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - : 'current', - : 'Elivco Kitchen Socket Current', + : 'power', + : 'Edesanya Energy Phase C power', : , - : , + : 'kW', }), 'context': , - 'entity_id': 'sensor.elivco_kitchen_socket_current', + 'entity_id': 'sensor.edesanya_energy_phase_c_power', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_power-entry] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_power_factor-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -7642,7 +7878,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.elivco_kitchen_socket_power', + 'entity_id': 'sensor.edesanya_energy_phase_c_power_factor', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -7650,48 +7886,44 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Power', + 'object_id_base': 'Phase C power factor', 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Power', + 'original_name': 'Phase C power factor', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'power', - 'unique_id': 'tuya.cq4hzlrnqn4qi0mqzccur_power', - 'unit_of_measurement': 'W', + 'translation_key': 'phase_c_power_factor', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_cpowerfactor', + 'unit_of_measurement': None, }) # --- -# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_power-state] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_power_factor-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - : 'power', - : 'Elivco Kitchen Socket Power', + : 'power_factor', + : 'Edesanya Energy Phase C power factor', : , - : 'W', }), 'context': , - 'entity_id': 'sensor.elivco_kitchen_socket_power', + 'entity_id': 'sensor.edesanya_energy_phase_c_power_factor', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_total_energy-entry] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_reactive_power-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, ]), 'area_id': None, 'capabilities': dict({ - : , + : , }), 'config_entry_id': , 'config_subentry_id': , @@ -7700,7 +7932,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.elivco_kitchen_socket_total_energy', + 'entity_id': 'sensor.edesanya_energy_phase_c_reactive_power', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -7708,44 +7940,44 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Total energy', + 'object_id_base': 'Phase C reactive power', 'options': dict({ 'sensor': dict({ 'suggested_display_precision': 2, }), 'sensor.private': dict({ - 'suggested_unit_of_measurement': , + 'suggested_unit_of_measurement': 'kvar', }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Total energy', + 'original_name': 'Phase C reactive power', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'total_energy', - 'unique_id': 'tuya.cq4hzlrnqn4qi0mqzcadd_ele', - 'unit_of_measurement': , + 'translation_key': 'phase_c_reactive_power', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_creactivepower', + 'unit_of_measurement': 'kvar', }) # --- -# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_total_energy-state] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_reactive_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - : 'energy', - : 'Elivco Kitchen Socket Total energy', - : , - : , + : 'reactive_power', + : 'Edesanya Energy Phase C reactive power', + : , + : 'kvar', }), 'context': , - 'entity_id': 'sensor.elivco_kitchen_socket_total_energy', + 'entity_id': 'sensor.edesanya_energy_phase_c_reactive_power', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.024', + 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_voltage-entry] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -7761,7 +7993,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.elivco_kitchen_socket_voltage', + 'entity_id': 'sensor.edesanya_energy_phase_c_voltage', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -7769,44 +8001,99 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Voltage', + 'object_id_base': 'Phase C voltage', 'options': dict({ 'sensor': dict({ 'suggested_display_precision': 0, }), - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Voltage', + 'original_name': 'Phase C voltage', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'voltage', - 'unique_id': 'tuya.cq4hzlrnqn4qi0mqzccur_voltage', - 'unit_of_measurement': , + 'translation_key': 'phase_c_voltage', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldphase_cvoltage', + 'unit_of_measurement': 'V', }) # --- -# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_voltage-state] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_phase_c_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ : 'voltage', - : 'Elivco Kitchen Socket Voltage', + : 'Edesanya Energy Phase C voltage', : , - : , + : 'V', }), 'context': , - 'entity_id': 'sensor.elivco_kitchen_socket_voltage', + 'entity_id': 'sensor.edesanya_energy_phase_c_voltage', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '233.4', + 'state': 'unknown', }) # --- -# name: test_platform_setup_and_discovery[sensor.elivco_tv_current-entry] +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_total_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.edesanya_energy_total_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total energy', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_energy', + 'unique_id': 'tuya.vcrfgwvbuybgnj3zqldtotal_forward_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.edesanya_energy_total_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'energy', + : 'Edesanya Energy Total energy', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.edesanya_energy_total_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '219.72', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_current-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -7822,7 +8109,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.elivco_tv_current', + 'entity_id': 'sensor.elivco_kitchen_socket_current', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -7847,27 +8134,27 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current', - 'unique_id': 'tuya.pz2xuth8hczv6zrwzccur_current', + 'unique_id': 'tuya.cq4hzlrnqn4qi0mqzccur_current', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[sensor.elivco_tv_current-state] +# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ : 'current', - : 'Elivco TV Current', + : 'Elivco Kitchen Socket Current', : , : , }), 'context': , - 'entity_id': 'sensor.elivco_tv_current', + 'entity_id': 'sensor.elivco_kitchen_socket_current', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.091', + 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[sensor.elivco_tv_power-entry] +# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_power-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -7883,7 +8170,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.elivco_tv_power', + 'entity_id': 'sensor.elivco_kitchen_socket_power', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -7905,27 +8192,27 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', - 'unique_id': 'tuya.pz2xuth8hczv6zrwzccur_power', + 'unique_id': 'tuya.cq4hzlrnqn4qi0mqzccur_power', 'unit_of_measurement': 'W', }) # --- -# name: test_platform_setup_and_discovery[sensor.elivco_tv_power-state] +# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ : 'power', - : 'Elivco TV Power', + : 'Elivco Kitchen Socket Power', : , : 'W', }), 'context': , - 'entity_id': 'sensor.elivco_tv_power', + 'entity_id': 'sensor.elivco_kitchen_socket_power', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '10.0', + 'state': '0.0', }) # --- -# name: test_platform_setup_and_discovery[sensor.elivco_tv_total_energy-entry] +# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_total_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -7941,7 +8228,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.elivco_tv_total_energy', + 'entity_id': 'sensor.elivco_kitchen_socket_total_energy', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -7966,27 +8253,27 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy', - 'unique_id': 'tuya.pz2xuth8hczv6zrwzcadd_ele', + 'unique_id': 'tuya.cq4hzlrnqn4qi0mqzcadd_ele', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[sensor.elivco_tv_total_energy-state] +# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_total_energy-state] StateSnapshot({ 'attributes': ReadOnlyDict({ : 'energy', - : 'Elivco TV Total energy', + : 'Elivco Kitchen Socket Total energy', : , : , }), 'context': , - 'entity_id': 'sensor.elivco_tv_total_energy', + 'entity_id': 'sensor.elivco_kitchen_socket_total_energy', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.006', + 'state': '0.024', }) # --- -# name: test_platform_setup_and_discovery[sensor.elivco_tv_voltage-entry] +# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -8002,7 +8289,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.elivco_tv_voltage', + 'entity_id': 'sensor.elivco_kitchen_socket_voltage', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -8027,34 +8314,34 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage', - 'unique_id': 'tuya.pz2xuth8hczv6zrwzccur_voltage', + 'unique_id': 'tuya.cq4hzlrnqn4qi0mqzccur_voltage', 'unit_of_measurement': , }) # --- -# name: test_platform_setup_and_discovery[sensor.elivco_tv_voltage-state] +# name: test_platform_setup_and_discovery[sensor.elivco_kitchen_socket_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ : 'voltage', - : 'Elivco TV Voltage', + : 'Elivco Kitchen Socket Voltage', : , : , }), 'context': , - 'entity_id': 'sensor.elivco_tv_voltage', + 'entity_id': 'sensor.elivco_kitchen_socket_voltage', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '237.7', + 'state': '233.4', }) # --- -# name: test_platform_setup_and_discovery[sensor.ev_charger_session_energy-entry] +# name: test_platform_setup_and_discovery[sensor.elivco_tv_current-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, ]), 'area_id': None, 'capabilities': dict({ - : , + : , }), 'config_entry_id': , 'config_subentry_id': , @@ -8063,7 +8350,248 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.ev_charger_session_energy', + 'entity_id': 'sensor.elivco_tv_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current', + 'unique_id': 'tuya.pz2xuth8hczv6zrwzccur_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_tv_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'current', + : 'Elivco TV Current', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.elivco_tv_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.091', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_tv_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.elivco_tv_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.pz2xuth8hczv6zrwzccur_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_tv_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'power', + : 'Elivco TV Power', + : , + : 'W', + }), + 'context': , + 'entity_id': 'sensor.elivco_tv_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_tv_total_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.elivco_tv_total_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total energy', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_energy', + 'unique_id': 'tuya.pz2xuth8hczv6zrwzcadd_ele', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_tv_total_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'energy', + : 'Elivco TV Total energy', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.elivco_tv_total_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.006', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_tv_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.elivco_tv_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage', + 'unique_id': 'tuya.pz2xuth8hczv6zrwzccur_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.elivco_tv_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'voltage', + : 'Elivco TV Voltage', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.elivco_tv_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '237.7', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.ev_charger_session_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ev_charger_session_energy', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -20135,7 +20663,7 @@ 'state': '1.17', }) # --- -# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_a_current-entry] +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_a_apparent_power-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -20151,7 +20679,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_a_current', + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_a_apparent_power', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -20159,38 +20687,96 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Phase A current', + 'object_id_base': 'Phase A apparent power', 'options': dict({ 'sensor': dict({ 'suggested_display_precision': 2, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Phase A current', + 'original_name': 'Phase A apparent power', 'platform': 'tuya', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'phase_a_current', - 'unique_id': 'tuya.jpry3yxd77xu5woibdnzphase_aelectriccurrent', - 'unit_of_measurement': 'A', + 'translation_key': 'phase_a_apparent_power', + 'unique_id': 'tuya.jpry3yxd77xu5woibdnzphase_aapparentpower', + 'unit_of_measurement': 'kVA', }) # --- -# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_a_current-state] +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_a_apparent_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - : 'current', - : '三相电能表 2 Phase A current', + : 'apparent_power', + : '三相电能表 2 Phase A apparent power', : , - : 'A', + : 'kVA', }), 'context': , - 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_a_current', + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_a_apparent_power', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '53.86', + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_a_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_a_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Phase A current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A current', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_current', + 'unique_id': 'tuya.jpry3yxd77xu5woibdnzphase_aelectriccurrent', + 'unit_of_measurement': 'A', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_a_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'current', + : '三相电能表 2 Phase A current', + : , + : 'A', + }), + 'context': , + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_a_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '53.86', }) # --- # name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_a_power-entry] @@ -20251,6 +20837,118 @@ 'state': '9.548', }) # --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_a_power_factor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_a_power_factor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Phase A power factor', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A power factor', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_power_factor', + 'unique_id': 'tuya.jpry3yxd77xu5woibdnzphase_apowerfactor', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_a_power_factor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'power_factor', + : '三相电能表 2 Phase A power factor', + : , + }), + 'context': , + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_a_power_factor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.77', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_a_reactive_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_a_reactive_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Phase A reactive power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase A reactive power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_a_reactive_power', + 'unique_id': 'tuya.jpry3yxd77xu5woibdnzphase_areactivepower', + 'unit_of_measurement': 'kvar', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_a_reactive_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'reactive_power', + : '三相电能表 2 Phase A reactive power', + : , + : 'kvar', + }), + 'context': , + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_a_reactive_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_a_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -20309,6 +21007,64 @@ 'state': '230.1', }) # --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_b_apparent_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_b_apparent_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Phase B apparent power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase B apparent power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_b_apparent_power', + 'unique_id': 'tuya.jpry3yxd77xu5woibdnzphase_bapparentpower', + 'unit_of_measurement': 'kVA', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_b_apparent_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'apparent_power', + : '三相电能表 2 Phase B apparent power', + : , + : 'kVA', + }), + 'context': , + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_b_apparent_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_b_current-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -20425,6 +21181,118 @@ 'state': '9.51', }) # --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_b_power_factor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_b_power_factor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Phase B power factor', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase B power factor', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_b_power_factor', + 'unique_id': 'tuya.jpry3yxd77xu5woibdnzphase_bpowerfactor', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_b_power_factor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'power_factor', + : '三相电能表 2 Phase B power factor', + : , + }), + 'context': , + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_b_power_factor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.83', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_b_reactive_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_b_reactive_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Phase B reactive power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase B reactive power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_b_reactive_power', + 'unique_id': 'tuya.jpry3yxd77xu5woibdnzphase_breactivepower', + 'unit_of_measurement': 'kvar', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_b_reactive_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'reactive_power', + : '三相电能表 2 Phase B reactive power', + : , + : 'kvar', + }), + 'context': , + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_b_reactive_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_b_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -20483,6 +21351,64 @@ 'state': '231.8', }) # --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_c_apparent_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_c_apparent_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Phase C apparent power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase C apparent power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_c_apparent_power', + 'unique_id': 'tuya.jpry3yxd77xu5woibdnzphase_capparentpower', + 'unit_of_measurement': 'kVA', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_c_apparent_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'apparent_power', + : '三相电能表 2 Phase C apparent power', + : , + : 'kVA', + }), + 'context': , + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_c_apparent_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_c_current-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -20599,6 +21525,118 @@ 'state': '10.006', }) # --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_c_power_factor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_c_power_factor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Phase C power factor', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase C power factor', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_c_power_factor', + 'unique_id': 'tuya.jpry3yxd77xu5woibdnzphase_cpowerfactor', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_c_power_factor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'power_factor', + : '三相电能表 2 Phase C power factor', + : , + }), + 'context': , + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_c_power_factor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.86', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_c_reactive_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_c_reactive_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Phase C reactive power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phase C reactive power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'phase_c_reactive_power', + 'unique_id': 'tuya.jpry3yxd77xu5woibdnzphase_creactivepower', + 'unit_of_measurement': 'kvar', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_c_reactive_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'reactive_power', + : '三相电能表 2 Phase C reactive power', + : , + : 'kvar', + }), + 'context': , + 'entity_id': 'sensor.san_xiang_dian_neng_biao_2_phase_c_reactive_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_platform_setup_and_discovery[sensor.san_xiang_dian_neng_biao_2_phase_c_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index a9b7dec7b72e8..37fca4dbbd3ff 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -1820,6 +1820,106 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[switch.c30_alarm_push_notifications-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.c30_alarm_push_notifications', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Alarm push notifications', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Alarm push notifications', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_push_notifications', + 'unique_id': 'tuya.rirsc4vhpbv2whkp2gwswitch_alarm_propel', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.c30_alarm_push_notifications-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'C30 Alarm push notifications', + }), + 'context': , + 'entity_id': 'switch.c30_alarm_push_notifications', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_platform_setup_and_discovery[switch.c30_keypad_sound-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.c30_keypad_sound', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Keypad sound', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Keypad sound', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'keypad_sound', + 'unique_id': 'tuya.rirsc4vhpbv2whkp2gwswitch_kb_sound', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.c30_keypad_sound-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'C30 Keypad sound', + }), + 'context': , + 'entity_id': 'switch.c30_keypad_sound', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_platform_setup_and_discovery[switch.c9_auto_trigger_siren-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/xbox/test_config_flow.py b/tests/components/xbox/test_config_flow.py index 0144f38dfb3b0..4a0c3ffba7b47 100644 --- a/tests/components/xbox/test_config_flow.py +++ b/tests/components/xbox/test_config_flow.py @@ -696,7 +696,7 @@ async def test_migration_exceptions( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR + assert config_entry.state is config_entries.ConfigEntryState.SETUP_RETRY @pytest.mark.usefixtures("xbox_live_client", "authentication_manager") @@ -731,7 +731,7 @@ async def test_migration_implementation_unavailable(hass: HomeAssistant) -> None await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR + assert config_entry.state is config_entries.ConfigEntryState.SETUP_RETRY @pytest.mark.usefixtures( diff --git a/tests/components/yalexs_ble/__init__.py b/tests/components/yalexs_ble/__init__.py index d6ce326cbe2e0..8baf337a681f9 100644 --- a/tests/components/yalexs_ble/__init__.py +++ b/tests/components/yalexs_ble/__init__.py @@ -41,6 +41,23 @@ tx_power=-127, ) +SAME_LOCAL_NAME_DISCOVERY_INFO = BluetoothServiceInfoBleak( + name="M1012LU", + address="A8:51:AB:91:1C:FA", + rssi=-40, + manufacturer_data={ + 76: b"\x061\x00Z\x8f\x93\xb2\xec\x85\x06\x00i\x00\x02\x02Q\xed\x1d\xf0" + }, + service_uuids=[], + service_data={}, + source="local", + device=generate_ble_device(address="A8:51:AB:91:1C:FA", name="M1012LU"), + advertisement=generate_advertisement_data(), + time=0, + connectable=True, + tx_power=-127, +) + OLD_FIRMWARE_LOCK_DISCOVERY_INFO = BluetoothServiceInfoBleak( name="Aug", address="AA:BB:CC:DD:EE:FF", diff --git a/tests/components/yalexs_ble/test_config_flow.py b/tests/components/yalexs_ble/test_config_flow.py index ad716261a39aa..725451d919752 100644 --- a/tests/components/yalexs_ble/test_config_flow.py +++ b/tests/components/yalexs_ble/test_config_flow.py @@ -23,6 +23,7 @@ LOCK_DISCOVERY_INFO_UUID_ADDRESS, NOT_YALE_DISCOVERY_INFO, OLD_FIRMWARE_LOCK_DISCOVERY_INFO, + SAME_LOCAL_NAME_DISCOVERY_INFO, YALE_ACCESS_LOCK_DISCOVERY_INFO, ) @@ -579,6 +580,41 @@ async def test_integration_discovery_success(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 +async def test_integration_discovery_prefers_address(hass: HomeAssistant) -> None: + """Test integration discovery binds to the address it was handed.""" + with patch( + "homeassistant.components.yalexs_ble.util.async_discovered_service_info", + # Another device is advertising the same cached local name, and it is + # seen before the lock itself + return_value=[ + SAME_LOCAL_NAME_DISCOVERY_INFO, + YALE_ACCESS_LOCK_DISCOVERY_INFO, + ], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + assert result["type"] is FlowResultType.FORM + + with patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["data"][CONF_ADDRESS] == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + + async def test_integration_discovery_device_not_found(hass: HomeAssistant) -> None: """Test integration discovery when the device is not found.""" with patch( diff --git a/tests/test_config.py b/tests/test_config.py index 5bf09eb309a39..6c4d2332998d7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1481,6 +1481,52 @@ async def test_component_config_validation_error_with_docs( assert error_records == snapshot +@pytest.mark.parametrize( + ("key", "expected"), + [ + pytest.param( + "command_topci", + "'command_topci' is an invalid option for 'mqtt' " + "(did you mean 'command_topic'?), check: command_topci", + id="one_candidate", + ), + pytest.param( + "command_t", + "'command_t' is an invalid option for 'mqtt' " + "(did you mean 'command_topic' or 'command_template'?), check: command_t", + id="two_candidates", + ), + pytest.param( + "totally_unrelated", + "'totally_unrelated' is an invalid option for 'mqtt', " + "check: totally_unrelated", + id="no_candidate", + ), + ], +) +async def test_stringify_invalid_suggests_close_keys( + hass: HomeAssistant, key: str, expected: str +) -> None: + """Test an unknown option reports the close matches probatio found.""" + schema = probatio.Schema( + { + probatio.Optional("command_topic"): str, + probatio.Optional("command_template"): str, + } + ) + config = {key: "some-value"} + + with pytest.raises(probatio.MultipleInvalid) as exc_info: + schema(config) + + assert ( + config_util.stringify_invalid( + hass, exc_info.value.errors[0], "mqtt", config, None, 500 + ) + == f"Invalid config for 'mqtt': {expected}" + ) + + @pytest.mark.parametrize( "config_dir", ["packages", "packages_include_dir_named"], diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 01e69615044d3..32adc595682cf 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -10,6 +10,7 @@ from typing import Any, Self from unittest.mock import ANY, AsyncMock, Mock, patch +from aiohttp import RequestInfo from freezegun.api import FrozenDateTimeFactory import probatio import pytest @@ -40,6 +41,7 @@ ConfigEntryError, ConfigEntryNotReady, HomeAssistantError, + OAuth2TokenRequestReauthError, ) from homeassistant.helpers import entity_registry as er, frame, issue_registry as ir from homeassistant.helpers.discovery_flow import DiscoveryKey @@ -498,6 +500,197 @@ async def async_step_user(self, user_input=None): ) +@pytest.mark.parametrize( + ( + "mock_migrate_entry", + "state", + "log_message", + "logs", + "translation_key", + "translation_domain", + ), + [ + pytest.param( + AsyncMock( + side_effect=ConfigEntryError( + translation_key="error", translation_domain="comp" + ) + ), + config_entries.ConfigEntryState.MIGRATION_ERROR, + "Error migrating entry Mock Title for comp", + True, + "error", + "comp", + id="ConfigEntryError", + ), + pytest.param( + AsyncMock( + side_effect=ConfigEntryAuthFailed( + translation_key="error", translation_domain="comp" + ) + ), + config_entries.ConfigEntryState.MIGRATION_ERROR, + "Config entry 'Mock Title' for comp integration could not authenticate", + True, + "error", + "comp", + id="ConfigEntryAuthFailed", + ), + pytest.param( + AsyncMock( + side_effect=ConfigEntryNotReady( + translation_key="error", translation_domain="comp" + ) + ), + config_entries.ConfigEntryState.SETUP_RETRY, + "Config entry migration 'Mock Title' for comp integration not ready yet", + True, + "error", + "comp", + id="ConfigEntryNotReady", + ), + pytest.param( + AsyncMock(side_effect=Exception()), + config_entries.ConfigEntryState.MIGRATION_ERROR, + "Error migrating entry Mock Title for comp", + True, + None, + None, + id="Other exceptions", + ), + pytest.param( + AsyncMock(return_value=False), + config_entries.ConfigEntryState.MIGRATION_ERROR, + "Error migrating entry Mock Title for comp", + False, # No logging + None, + None, + id="Returns False", + ), + pytest.param( + AsyncMock( + side_effect=OAuth2TokenRequestReauthError( + domain="comp", + request_info=RequestInfo( + url="https://example.com", + method="GET", + headers={}, + real_url="https://example.com", + ), + ) + ), + config_entries.ConfigEntryState.MIGRATION_ERROR, + "Config entry 'Mock Title' for comp integration could not authenticate", + True, + "oauth2_helper_reauth_required", + "homeassistant", + id="OAuth reauth error", + ), + ], +) +async def test_migrate_handle_exceptions( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mock_migrate_entry: AsyncMock, + state: config_entries.ConfigEntryState, + log_message: str, + logs: bool, + translation_key: str | None, + translation_domain: str | None, +) -> None: + """Test migration handles exceptions correctly.""" + entry = MockConfigEntry(domain="comp", version=1, minor_version=1) + entry.add_to_hass(hass) + assert not entry.supports_unload + + mock_setup_entry = AsyncMock(return_value=True) + + mock_integration( + hass, + MockModule( + "comp", + async_setup_entry=mock_setup_entry, + async_migrate_entry=mock_migrate_entry, + ), + ) + mock_platform(hass, "comp.config_flow", None) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 2 + MINOR_VERSION = 1 + + async def async_step_user(self, user_input=None): + """Test user step.""" + return self.async_create_entry(title="title", data={}) + + with mock_config_flow("comp", TestFlow): + result = await async_setup_component(hass, "comp", {}) + assert result + assert len(mock_setup_entry.mock_calls) == 0 + assert entry.state is state + assert entry.error_reason_translation_domain == translation_domain + assert entry.error_reason_translation_key == translation_key + assert (log_message in caplog.text) is logs + + assert hass.config_entries.flow.async_progress_by_handler("comp") == [] + + +async def test_migrate_raise_configentrynotready( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, +) -> None: + """Test migration can retry later when raise ConfigEntryNotReady.""" + entry = MockConfigEntry(domain="comp", version=1, minor_version=1) + entry.add_to_hass(hass) + assert not entry.supports_unload + + mock_setup_entry = AsyncMock(return_value=True) + + mock_migrate_entry = AsyncMock( + side_effect=[ConfigEntryNotReady("Migration not ready yet"), True] + ) + + mock_integration( + hass, + MockModule( + "comp", + async_setup_entry=mock_setup_entry, + async_migrate_entry=mock_migrate_entry, + ), + ) + mock_platform(hass, "comp.config_flow", None) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 2 + MINOR_VERSION = 1 + + async def async_step_user(self, user_input=None): + """Test user step.""" + return self.async_create_entry(title="title", data={}) + + with mock_config_flow("comp", TestFlow): + result = await async_setup_component(hass, "comp", {}) + assert result + assert len(mock_setup_entry.mock_calls) == 0 + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert ( + "Config entry migration 'Mock Title' for comp integration not ready yet" + in caplog.text + ) + + freezer.tick(timedelta(seconds=60)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 1 + assert entry.state is config_entries.ConfigEntryState.LOADED + + @pytest.mark.parametrize(("major_version", "minor_version"), [(2, 1), (2, 2)]) async def test_call_async_migrate_entry_failure_not_supported( hass: HomeAssistant, major_version: int, minor_version: int