diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index 1ebeb596044f22..d8feff1a59d807 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["aioasuswrt", "asusrouter", "asyncssh"], - "requirements": ["aioasuswrt==1.5.1", "asusrouter==1.21.1"] + "requirements": ["aioasuswrt==1.5.2", "asusrouter==1.21.3"] } diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index f903065a124335..3519766e0bf826 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -64,6 +64,12 @@ async def async_migrate_entry(hass: HomeAssistant, entry: BlinkConfigEntry) -> b if entry.version == 2: await _reauth_flow_wrapper(hass, entry, data) return False + if entry.version == 3: + # Migrate device_id to hardware_id for blinkpy 0.25.x OAuth2 compatibility + if "device_id" in data: + data["hardware_id"] = data.pop("device_id") + hass.config_entries.async_update_entry(entry, data=data, version=4) + return True return True diff --git a/homeassistant/components/blink/config_flow.py b/homeassistant/components/blink/config_flow.py index f4d393ed8b5b9d..896226327af8dd 100644 --- a/homeassistant/components/blink/config_flow.py +++ b/homeassistant/components/blink/config_flow.py @@ -21,7 +21,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DEVICE_ID, DOMAIN +from .const import DOMAIN, HARDWARE_ID _LOGGER = logging.getLogger(__name__) @@ -43,7 +43,7 @@ async def _send_blink_2fa_pin(blink: Blink, pin: str | None) -> bool: class BlinkConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a Blink config flow.""" - VERSION = 3 + VERSION = 4 def __init__(self) -> None: """Initialize the blink flow.""" @@ -53,7 +53,7 @@ def __init__(self) -> None: async def _handle_user_input(self, user_input: dict[str, Any]): """Handle user input.""" self.auth = Auth( - {**user_input, "device_id": DEVICE_ID}, + {**user_input, "hardware_id": HARDWARE_ID}, no_prompt=True, session=async_get_clientsession(self.hass), ) diff --git a/homeassistant/components/blink/const.py b/homeassistant/components/blink/const.py index 3e4ffeeea072df..e57a05822e480d 100644 --- a/homeassistant/components/blink/const.py +++ b/homeassistant/components/blink/const.py @@ -3,7 +3,7 @@ from homeassistant.const import Platform DOMAIN = "blink" -DEVICE_ID = "Home Assistant" +HARDWARE_ID = "Home Assistant" CONF_MIGRATE = "migrate" CONF_CAMERA = "camera" diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index 6a596fb5f2f245..c216df245e5c12 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/blink", "iot_class": "cloud_polling", "loggers": ["blinkpy"], - "requirements": ["blinkpy==0.24.1"] + "requirements": ["blinkpy==0.25.1"] } diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index c828b971b99d52..5b619139d5e3f5 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -8,5 +8,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==11.1.0"] + "requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==12.1.1"] } diff --git a/homeassistant/components/google_air_quality/manifest.json b/homeassistant/components/google_air_quality/manifest.json index 22789aceb92698..084151d266771c 100644 --- a/homeassistant/components/google_air_quality/manifest.json +++ b/homeassistant/components/google_air_quality/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["google_air_quality_api"], "quality_scale": "bronze", - "requirements": ["google_air_quality_api==2.0.0"] + "requirements": ["google_air_quality_api==2.0.2"] } diff --git a/homeassistant/components/google_air_quality/strings.json b/homeassistant/components/google_air_quality/strings.json index a3c32f1b6ba544..6ed0a11e041a26 100644 --- a/homeassistant/components/google_air_quality/strings.json +++ b/homeassistant/components/google_air_quality/strings.json @@ -88,16 +88,16 @@ "1b_good_air_quality": "1B - Good air quality", "2_cyan": "2 - Cyan", "2_light_green": "2 - Light green", - "2_orange": "4 - Orange", - "2_red": "5 - Red", - "2_yellow": "3 - Yellow", "2a_acceptable_air_quality": "2A - Acceptable air quality", "2b_acceptable_air_quality": "2B - Acceptable air quality", "3_green": "3 - Green", + "3_yellow": "3 - Yellow", "3a_aggravated_air_quality": "3A - Aggravated air quality", "3b_bad_air_quality": "3B - Bad air quality", + "4_orange": "4 - Orange", "4_yellow_watch": "4 - Yellow/Watch", "5_orange_alert": "5 - Orange/Alert", + "5_red": "5 - Red", "6_red_alert": "6 - Red/Alert+", "10_33": "10-33% of guideline", "33_66": "33-66% of guideline", diff --git a/homeassistant/components/growatt_server/sensor/mix.py b/homeassistant/components/growatt_server/sensor/mix.py index b741a589b8f521..910ec447b230c9 100644 --- a/homeassistant/components/growatt_server/sensor/mix.py +++ b/homeassistant/components/growatt_server/sensor/mix.py @@ -27,6 +27,7 @@ api_key="eBatChargeToday", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), GrowattSensorEntityDescription( key="mix_battery_charge_lifetime", @@ -42,6 +43,7 @@ api_key="eBatDisChargeToday", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), GrowattSensorEntityDescription( key="mix_battery_discharge_lifetime", @@ -57,6 +59,7 @@ api_key="epvToday", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), GrowattSensorEntityDescription( key="mix_solar_generation_lifetime", @@ -72,6 +75,7 @@ api_key="pDischarge1", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), GrowattSensorEntityDescription( key="mix_battery_voltage", @@ -101,6 +105,7 @@ api_key="elocalLoadToday", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), GrowattSensorEntityDescription( key="mix_load_consumption_lifetime", @@ -116,6 +121,7 @@ api_key="etoGridToday", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), GrowattSensorEntityDescription( key="mix_export_to_grid_lifetime", @@ -132,6 +138,7 @@ api_key="chargePower", native_unit_of_measurement=UnitOfPower.KILO_WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), GrowattSensorEntityDescription( key="mix_load_consumption", @@ -139,6 +146,7 @@ api_key="pLocalLoad", native_unit_of_measurement=UnitOfPower.KILO_WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), GrowattSensorEntityDescription( key="mix_wattage_pv_1", @@ -146,6 +154,7 @@ api_key="pPv1", native_unit_of_measurement=UnitOfPower.KILO_WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), GrowattSensorEntityDescription( key="mix_wattage_pv_2", @@ -153,6 +162,7 @@ api_key="pPv2", native_unit_of_measurement=UnitOfPower.KILO_WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), GrowattSensorEntityDescription( key="mix_wattage_pv_all", @@ -160,6 +170,7 @@ api_key="ppv", native_unit_of_measurement=UnitOfPower.KILO_WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), GrowattSensorEntityDescription( key="mix_export_to_grid", @@ -167,6 +178,7 @@ api_key="pactogrid", native_unit_of_measurement=UnitOfPower.KILO_WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), GrowattSensorEntityDescription( key="mix_import_from_grid", @@ -174,6 +186,7 @@ api_key="pactouser", native_unit_of_measurement=UnitOfPower.KILO_WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), GrowattSensorEntityDescription( key="mix_battery_discharge_kw", @@ -181,6 +194,7 @@ api_key="pdisCharge1", native_unit_of_measurement=UnitOfPower.KILO_WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), GrowattSensorEntityDescription( key="mix_grid_voltage", @@ -196,6 +210,7 @@ api_key="eCharge", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), GrowattSensorEntityDescription( key="mix_load_consumption_solar_today", @@ -203,6 +218,7 @@ api_key="eChargeToday", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), GrowattSensorEntityDescription( key="mix_self_consumption_today", @@ -210,6 +226,7 @@ api_key="eChargeToday1", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), GrowattSensorEntityDescription( key="mix_load_consumption_battery_today", @@ -217,6 +234,7 @@ api_key="echarge1", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), GrowattSensorEntityDescription( key="mix_import_from_grid_today", @@ -224,6 +242,7 @@ api_key="etouser", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), # This sensor is manually created using the most recent X-Axis value from the chartData GrowattSensorEntityDescription( diff --git a/homeassistant/components/growatt_server/sensor/tlx.py b/homeassistant/components/growatt_server/sensor/tlx.py index 298170531de301..e3689fbf7d195f 100644 --- a/homeassistant/components/growatt_server/sensor/tlx.py +++ b/homeassistant/components/growatt_server/sensor/tlx.py @@ -79,6 +79,7 @@ api_key="ppv1", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, precision=1, ), GrowattSensorEntityDescription( @@ -122,6 +123,7 @@ api_key="ppv2", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, precision=1, ), GrowattSensorEntityDescription( @@ -165,6 +167,7 @@ api_key="ppv3", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, precision=1, ), GrowattSensorEntityDescription( @@ -208,6 +211,7 @@ api_key="ppv4", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, precision=1, ), GrowattSensorEntityDescription( @@ -234,6 +238,7 @@ api_key="ppv", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, precision=1, ), GrowattSensorEntityDescription( @@ -258,6 +263,7 @@ api_key="pac", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, precision=1, ), GrowattSensorEntityDescription( @@ -323,6 +329,7 @@ api_key="bdc1DischargePower", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), GrowattSensorEntityDescription( key="tlx_battery_1_discharge_total", @@ -339,6 +346,7 @@ api_key="bdc2DischargePower", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), GrowattSensorEntityDescription( key="tlx_battery_2_discharge_total", @@ -372,6 +380,7 @@ api_key="bdc1ChargePower", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), GrowattSensorEntityDescription( key="tlx_battery_1_charge_total", @@ -388,6 +397,7 @@ api_key="bdc2ChargePower", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), GrowattSensorEntityDescription( key="tlx_battery_2_charge_total", @@ -445,6 +455,7 @@ api_key="pacToLocalLoad", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, precision=1, ), GrowattSensorEntityDescription( @@ -453,6 +464,7 @@ api_key="pacToUserTotal", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, precision=1, ), GrowattSensorEntityDescription( @@ -461,6 +473,7 @@ api_key="pacToGridTotal", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, precision=1, ), GrowattSensorEntityDescription( @@ -545,6 +558,7 @@ api_key="psystem", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, precision=1, ), GrowattSensorEntityDescription( @@ -553,6 +567,7 @@ api_key="pself", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, precision=1, ), ) diff --git a/homeassistant/components/growatt_server/sensor/total.py b/homeassistant/components/growatt_server/sensor/total.py index 578745c861032e..a1eb898ae1cd7b 100644 --- a/homeassistant/components/growatt_server/sensor/total.py +++ b/homeassistant/components/growatt_server/sensor/total.py @@ -50,5 +50,6 @@ api_key="nominalPower", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), ) diff --git a/homeassistant/components/hanna/manifest.json b/homeassistant/components/hanna/manifest.json index b1e503e5e28f66..ffa44dfd6a8558 100644 --- a/homeassistant/components/hanna/manifest.json +++ b/homeassistant/components/hanna/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/hanna", "iot_class": "cloud_polling", "quality_scale": "bronze", - "requirements": ["hanna-cloud==0.0.6"] + "requirements": ["hanna-cloud==0.0.7"] } diff --git a/homeassistant/components/knx/services.py b/homeassistant/components/knx/services.py index f63612f97efaa0..ebb01e0ef28571 100644 --- a/homeassistant/components/knx/services.py +++ b/homeassistant/components/knx/services.py @@ -39,6 +39,10 @@ _LOGGER = logging.getLogger(__name__) +_DESCRIPTION_PLACEHOLDERS = { + "sensor_value_types_url": "https://www.home-assistant.io/integrations/knx/#value-types" +} + @callback def async_setup_services(hass: HomeAssistant) -> None: @@ -48,6 +52,7 @@ def async_setup_services(hass: HomeAssistant) -> None: SERVICE_KNX_SEND, service_send_to_knx_bus, schema=SERVICE_KNX_SEND_SCHEMA, + description_placeholders=_DESCRIPTION_PLACEHOLDERS, ) hass.services.async_register( @@ -63,6 +68,7 @@ def async_setup_services(hass: HomeAssistant) -> None: SERVICE_KNX_EVENT_REGISTER, service_event_register_modify, schema=SERVICE_KNX_EVENT_REGISTER_SCHEMA, + description_placeholders=_DESCRIPTION_PLACEHOLDERS, ) async_register_admin_service( @@ -71,6 +77,7 @@ def async_setup_services(hass: HomeAssistant) -> None: SERVICE_KNX_EXPOSURE_REGISTER, service_exposure_register_modify, schema=SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA, + description_placeholders=_DESCRIPTION_PLACEHOLDERS, ) async_register_admin_service( diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 200b90a49e00ba..fc6c04b318b9cc 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -674,7 +674,7 @@ "name": "Remove event registration" }, "type": { - "description": "If set, the payload will be decoded as given DPT in the event data `value` key. KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types).", + "description": "If set, the payload will be decoded as given DPT in the event data `value` key. KNX sensor types are valid values (see {sensor_value_types_url}).", "name": "Value type" } }, @@ -704,7 +704,7 @@ "name": "Remove exposure" }, "type": { - "description": "Telegrams will be encoded as given DPT. 'binary' and all KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types).", + "description": "Telegrams will be encoded as given DPT. 'binary' and all KNX sensor types are valid values (see {sensor_value_types_url}).", "name": "Value type" } }, @@ -740,7 +740,7 @@ "name": "Send as Response" }, "type": { - "description": "If set, the payload will not be sent as raw bytes, but encoded as given DPT. KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types).", + "description": "If set, the payload will not be sent as raw bytes, but encoded as given DPT. KNX sensor types are valid values (see {sensor_value_types_url}).", "name": "Value type" } }, diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index dbc663ed462538..1ea15e0072fe6e 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.2.2"] + "requirements": ["pylamarzocco==2.2.4"] } diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 7cde12207cf64f..a5c1c0f828d83d 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_polling", "loggers": ["pypck"], "quality_scale": "silver", - "requirements": ["pypck==0.9.5", "lcn-frontend==0.2.7"] + "requirements": ["pypck==0.9.7", "lcn-frontend==0.2.7"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 3676117f488aa9..58cf7ebc9d728e 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==11.1.0"] + "requirements": ["ical==12.1.1"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 6278ffd9a61433..81e895c8df4cbb 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==11.1.0"] + "requirements": ["ical==12.1.1"] } diff --git a/homeassistant/components/matter/select.py b/homeassistant/components/matter/select.py index a441160b02a4b8..ee6b537e9b3052 100644 --- a/homeassistant/components/matter/select.py +++ b/homeassistant/components/matter/select.py @@ -183,6 +183,48 @@ def _update_from_device(self) -> None: self._attr_name = desc +class MatterDoorLockOperatingModeSelectEntity(MatterAttributeSelectEntity): + """Representation of a Door Lock Operating Mode select entity. + + This entity dynamically filters available operating modes based on the device's + `SupportedOperatingModes` bitmap attribute. In this bitmap, bit=0 indicates a + supported mode and bit=1 indicates unsupported (inverted from typical bitmap conventions). + If the bitmap is unavailable, only mandatory modes are included. The mapping from + bitmap bits to operating mode values is defined by the Matter specification. + """ + + entity_description: MatterMapSelectEntityDescription + + @callback + def _update_from_device(self) -> None: + """Update from device.""" + # Get the bitmap of supported operating modes + supported_modes_bitmap = self.get_matter_attribute_value( + self.entity_description.list_attribute + ) + + # Convert bitmap to list of supported mode values + # NOTE: The Matter spec inverts the usual meaning: bit=0 means supported, + # bit=1 means not supported, undefined bits must be 1. Mandatory modes are + # bits 0 (Normal) and 3 (NoRemoteLockUnlock). + num_mode_bits = supported_modes_bitmap.bit_length() + supported_mode_values = [ + bit_position + for bit_position in range(num_mode_bits) + if not supported_modes_bitmap & (1 << bit_position) + ] + + # Map supported mode values to their string representations + self._attr_options = [ + mapped_value + for mode_value in supported_mode_values + if (mapped_value := self.entity_description.device_to_ha(mode_value)) + ] + + # Use base implementation to set the current option + super()._update_from_device() + + class MatterListSelectEntity(MatterEntity, SelectEntity): """Representation of a select entity from Matter list and selected item Cluster attribute(s).""" @@ -594,15 +636,18 @@ def _update_from_device(self) -> None: ), MatterDiscoverySchema( platform=Platform.SELECT, - entity_description=MatterSelectEntityDescription( + entity_description=MatterMapSelectEntityDescription( key="DoorLockOperatingMode", entity_category=EntityCategory.CONFIG, translation_key="door_lock_operating_mode", - options=list(DOOR_LOCK_OPERATING_MODE_MAP.values()), + list_attribute=clusters.DoorLock.Attributes.SupportedOperatingModes, device_to_ha=DOOR_LOCK_OPERATING_MODE_MAP.get, ha_to_device=DOOR_LOCK_OPERATING_MODE_MAP_REVERSE.get, ), - entity_class=MatterAttributeSelectEntity, - required_attributes=(clusters.DoorLock.Attributes.OperatingMode,), + entity_class=MatterDoorLockOperatingModeSelectEntity, + required_attributes=( + clusters.DoorLock.Attributes.OperatingMode, + clusters.DoorLock.Attributes.SupportedOperatingModes, + ), ), ] diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 16c396c0bb61f1..b894bae7db5707 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -46,7 +46,7 @@ "ws_path": "WebSocket path" }, "data_description": { - "advanced_options": "Enable and select **Next** to set advanced options.", + "advanced_options": "Enable and select **Submit** to set advanced options.", "broker": "The hostname or IP address of your MQTT broker.", "certificate": "The custom CA certificate file to validate your MQTT brokers certificate.", "client_cert": "The client certificate to authenticate against your MQTT broker.", diff --git a/homeassistant/components/ohme/sensor.py b/homeassistant/components/ohme/sensor.py index 930639660504a2..82c7476141658e 100644 --- a/homeassistant/components/ohme/sensor.py +++ b/homeassistant/components/ohme/sensor.py @@ -49,6 +49,7 @@ class OhmeSensorDescription(OhmeEntityDescription, SensorEntityDescription): key="current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + state_class=SensorStateClass.MEASUREMENT, value_fn=lambda client: client.power.amps, ), OhmeSensorDescription( @@ -57,6 +58,7 @@ class OhmeSensorDescription(OhmeEntityDescription, SensorEntityDescription): native_unit_of_measurement=UnitOfPower.WATT, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=1, + state_class=SensorStateClass.MEASUREMENT, value_fn=lambda client: client.power.watts, ), OhmeSensorDescription( @@ -81,6 +83,7 @@ class OhmeSensorDescription(OhmeEntityDescription, SensorEntityDescription): native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, suggested_display_precision=0, + state_class=SensorStateClass.MEASUREMENT, value_fn=lambda client: client.battery, ), OhmeSensorDescription( diff --git a/homeassistant/components/remote_calendar/manifest.json b/homeassistant/components/remote_calendar/manifest.json index 392d435d8578e6..6286e5548a8fb2 100644 --- a/homeassistant/components/remote_calendar/manifest.json +++ b/homeassistant/components/remote_calendar/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["ical"], "quality_scale": "silver", - "requirements": ["ical==11.1.0"] + "requirements": ["ical==12.1.1"] } diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index 54911795cd80f7..844323ca6f3ddb 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -394,7 +394,14 @@ def __init__( async def _async_update_data( self, ) -> dict[RoborockZeoProtocol, StateType]: - return await self.api.query_values(self.request_protocols) + try: + return await self.api.query_values(self.request_protocols) + except RoborockException as ex: + _LOGGER.debug("Failed to update washing machine data: %s", ex) + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="update_data_fail", + ) from ex class RoborockWetDryVacUpdateCoordinator( @@ -425,4 +432,11 @@ def __init__( async def _async_update_data( self, ) -> dict[RoborockDyadDataProtocol, StateType]: - return await self.api.query_values(self.request_protocols) + try: + return await self.api.query_values(self.request_protocols) + except RoborockException as ex: + _LOGGER.debug("Failed to update wet dry vac data: %s", ex) + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="update_data_fail", + ) from ex diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 090a498b751fca..9178d58eb683a3 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -20,7 +20,7 @@ "loggers": ["roborock"], "quality_scale": "silver", "requirements": [ - "python-roborock==3.10.10", + "python-roborock==3.12.2", "vacuum-map-parser-roborock==0.1.4" ] } diff --git a/homeassistant/components/roborock/time.py b/homeassistant/components/roborock/time.py index 1464db6e398e40..39d224b5dad72c 100644 --- a/homeassistant/components/roborock/time.py +++ b/homeassistant/components/roborock/time.py @@ -7,7 +7,7 @@ import logging from typing import Any -from roborock.data import DnDTimer +from roborock.data import DnDTimer, ValleyElectricityTimer from roborock.exceptions import RoborockException from homeassistant.components.time import TimeEntity, TimeEntityDescription @@ -80,13 +80,14 @@ class RoborockTimeDescription(TimeEntityDescription): key="off_peak_start", translation_key="off_peak_start", trait=lambda api: api.valley_electricity_timer, - update_value=lambda trait, desired_time: trait.update_value( - [ - desired_time.hour, - desired_time.minute, - trait.end_hour, - trait.end_minute, - ] + update_value=lambda trait, desired_time: trait.set_timer( + ValleyElectricityTimer( + enabled=trait.enabled, + start_hour=desired_time.hour, + start_minute=desired_time.minute, + end_hour=trait.end_hour, + end_minute=trait.end_minute, + ) ), get_value=lambda trait: datetime.time( hour=trait.start_hour, minute=trait.start_minute @@ -98,13 +99,14 @@ class RoborockTimeDescription(TimeEntityDescription): key="off_peak_end", translation_key="off_peak_end", trait=lambda api: api.valley_electricity_timer, - update_value=lambda trait, desired_time: trait.update_value( - [ - trait.start_hour, - trait.start_minute, - desired_time.hour, - desired_time.minute, - ] + update_value=lambda trait, desired_time: trait.set_timer( + ValleyElectricityTimer( + enabled=trait.enabled, + start_hour=trait.start_hour, + start_minute=trait.start_minute, + end_hour=desired_time.hour, + end_minute=desired_time.minute, + ) ), get_value=lambda trait: datetime.time( hour=trait.end_hour, minute=trait.end_minute diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 1b431a43479deb..2490404e41f9ad 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -31,5 +31,5 @@ "iot_class": "cloud_push", "loggers": ["pysmartthings"], "quality_scale": "bronze", - "requirements": ["pysmartthings==3.5.0"] + "requirements": ["pysmartthings==3.5.1"] } diff --git a/homeassistant/components/switchbot_cloud/binary_sensor.py b/homeassistant/components/switchbot_cloud/binary_sensor.py index 2a892206d8f02e..316badc42f7365 100644 --- a/homeassistant/components/switchbot_cloud/binary_sensor.py +++ b/homeassistant/components/switchbot_cloud/binary_sensor.py @@ -147,16 +147,16 @@ def __init__( self.entity_description = description self._attr_unique_id = f"{device.device_id}_{description.key}" - @property - def is_on(self) -> bool | None: + def _set_attributes(self) -> None: """Set attributes from coordinator data.""" if not self.coordinator.data: - return None + return if self.entity_description.value_fn: - return self.entity_description.value_fn(self.coordinator.data) + self._attr_is_on = self.entity_description.value_fn(self.coordinator.data) + return - return ( + self._attr_is_on = ( self.coordinator.data.get(self.entity_description.key) == self.entity_description.on_value ) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 77e6ab421d45cc..5c639f15e10aa2 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -433,23 +433,23 @@ def target_humidity(self) -> int | None: return self._read_wrapper(self._target_humidity_wrapper) @property - def hvac_mode(self) -> HVACMode: + def hvac_mode(self) -> HVACMode | None: """Return hvac mode.""" - # If the switch is off, hvac mode is off as well. - # Unless the switch doesn't exists of course... + # If the switch is off, hvac mode is off. + switch_status: bool | None if (switch_status := self._read_wrapper(self._switch_wrapper)) is False: return HVACMode.OFF - # If the mode is known and maps to an HVAC mode, return it. - if (mode := self._read_wrapper(self._hvac_mode_wrapper)) and ( - hvac_mode := TUYA_HVAC_TO_HA.get(mode) - ): - return hvac_mode + # If we don't have a mode wrapper, return switch only mode. + if self._hvac_mode_wrapper is None: + if switch_status is True: + return self.entity_description.switch_only_hvac_mode + return None - # If hvac_mode is unknown, return the switch only mode. - if switch_status: - return self.entity_description.switch_only_hvac_mode - return HVACMode.OFF + # If we do have a mode wrapper, check if the mode maps to an HVAC mode. + if (hvac_status := self._read_wrapper(self._hvac_mode_wrapper)) is None: + return None + return TUYA_HVAC_TO_HA.get(hvac_status) @property def preset_mode(self) -> str | None: diff --git a/homeassistant/components/tuya/diagnostics.py b/homeassistant/components/tuya/diagnostics.py index a4f5ed0711239e..6208e90e806aa2 100644 --- a/homeassistant/components/tuya/diagnostics.py +++ b/homeassistant/components/tuya/diagnostics.py @@ -98,6 +98,7 @@ def _async_device_as_dict( "home_assistant": {}, "set_up": device.set_up, "support_local": device.support_local, + "local_strategy": device.local_strategy, "warnings": DEVICE_WARNINGS.get(device.id), } diff --git a/homeassistant/components/tuya/models.py b/homeassistant/components/tuya/models.py index 5f1c5fa8dcd12e..e67d5f192693b0 100644 --- a/homeassistant/components/tuya/models.py +++ b/homeassistant/components/tuya/models.py @@ -127,12 +127,12 @@ class BitmapTypeInformation(TypeInformation): @classmethod def from_json(cls, dpcode: str, type_data: str) -> Self | None: """Load JSON string and return a BitmapTypeInformation object.""" - if not (parsed := json_loads_object(type_data)): + if not (parsed := cast(dict[str, Any] | None, json_loads_object(type_data))): return None return cls( dpcode=dpcode, type_data=type_data, - **cast(dict[str, list[str]], parsed), + label=parsed["label"], ) diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py index 3cc27a6f7e1380..f651a56b2dd050 100644 --- a/homeassistant/components/webhook/trigger.py +++ b/homeassistant/components/webhook/trigger.py @@ -15,6 +15,7 @@ from homeassistant.helpers.template import Template from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.util.json import json_loads from . import ( DEFAULT_METHODS, @@ -62,7 +63,9 @@ async def _handle_webhook( base_result: dict[str, Any] = {"platform": "webhook", "webhook_id": webhook_id} if "json" in request.headers.get(hdrs.CONTENT_TYPE, ""): - base_result["json"] = await request.json() + # Always attempt to read the body; request.text() returns "" if empty + text = await request.text() + base_result["json"] = json_loads(text) if text else {} else: base_result["data"] = await request.post() diff --git a/homeassistant/components/xbox/coordinator.py b/homeassistant/components/xbox/coordinator.py index 7666fee3620bc2..9e72ee6dd742ae 100644 --- a/homeassistant/components/xbox/coordinator.py +++ b/homeassistant/components/xbox/coordinator.py @@ -81,10 +81,11 @@ def __init__( _LOGGER, config_entry=config_entry, name=DOMAIN, - update_interval=timedelta(seconds=10), + update_interval=timedelta(seconds=15), ) self.data = XboxData() self.current_friends: set[str] = set() + self.title_data: dict[str, Title] = {} async def _async_setup(self) -> None: """Set up coordinator.""" @@ -217,7 +218,6 @@ async def _async_update_data(self) -> XboxData: ) # retrieve title details - title_data: dict[str, Title] = {} for person in presence_data.values(): if presence_detail := next( ( @@ -227,6 +227,12 @@ async def _async_update_data(self) -> XboxData: ), None, ): + if ( + person.xuid in self.title_data + and presence_detail.title_id + == self.title_data[person.xuid].title_id + ): + continue try: title = await self.client.titlehub.get_title_info( presence_detail.title_id @@ -250,7 +256,9 @@ async def _async_update_data(self) -> XboxData: translation_domain=DOMAIN, translation_key="request_exception", ) from e - title_data[person.xuid] = title.titles[0] + self.title_data[person.xuid] = title.titles[0] + else: + self.title_data.pop(person.xuid, None) person.last_seen_date_time_utc = self.last_seen_timestamp(person) if ( self.current_friends - (new_friends := set(presence_data)) @@ -259,7 +267,7 @@ async def _async_update_data(self) -> XboxData: self.remove_stale_devices(new_friends) self.current_friends = new_friends - return XboxData(new_console_data, presence_data, title_data) + return XboxData(new_console_data, presence_data, self.title_data) def last_seen_timestamp(self, person: Person) -> datetime | None: """Returns the most recent of two timestamps.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index 2f93fc0ebabce8..f925788648c33e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -17,7 +17,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e95428225aa6fc..7a4a386348e782 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -223,3 +223,6 @@ gql<4.0.0 # Pin pytest-rerunfailures to prevent accidental breaks pytest-rerunfailures==16.0.1 + +# pycares 5.x is not yet compatible with aiodns +pycares==4.11.0 diff --git a/pyproject.toml b/pyproject.toml index ecb7ee52ba23b3..a15b883986187b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.12.2" +version = "2025.12.3" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." diff --git a/requirements_all.txt b/requirements_all.txt index 24d87fcbe50fe3..d5e48cdc54df7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -206,7 +206,7 @@ aioaquacell==0.2.0 aioaseko==1.0.0 # homeassistant.components.asuswrt -aioasuswrt==1.5.1 +aioasuswrt==1.5.2 # homeassistant.components.husqvarna_automower aioautomower==2.7.1 @@ -537,7 +537,7 @@ arris-tg2492lg==2.2.0 asmog==0.0.6 # homeassistant.components.asuswrt -asusrouter==1.21.1 +asusrouter==1.21.3 # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms @@ -648,7 +648,7 @@ bleak==1.0.1 blebox-uniapi==2.5.0 # homeassistant.components.blink -blinkpy==0.24.1 +blinkpy==0.25.1 # homeassistant.components.bitcoin blockchain==1.4.4 @@ -1090,7 +1090,7 @@ google-nest-sdm==9.1.2 google-photos-library-api==0.12.1 # homeassistant.components.google_air_quality -google_air_quality_api==2.0.0 +google_air_quality_api==2.0.2 # homeassistant.components.slide # homeassistant.components.slide_local @@ -1154,7 +1154,7 @@ habiticalib==0.4.6 habluetooth==5.7.0 # homeassistant.components.hanna -hanna-cloud==0.0.6 +hanna-cloud==0.0.7 # homeassistant.components.cloud hass-nabucasa==1.7.0 @@ -1234,7 +1234,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==11.1.0 +ical==12.1.1 # homeassistant.components.caldav icalendar==6.3.1 @@ -2145,7 +2145,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.2.2 +pylamarzocco==2.2.4 # homeassistant.components.lastfm pylast==5.1.0 @@ -2285,7 +2285,7 @@ pypaperless==4.1.1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.9.5 +pypck==0.9.7 # homeassistant.components.pglab pypglab==0.0.5 @@ -2400,7 +2400,7 @@ pysmappee==0.2.29 pysmarlaapi==0.9.2 # homeassistant.components.smartthings -pysmartthings==3.5.0 +pysmartthings==3.5.1 # homeassistant.components.smarty pysmarty2==0.10.3 @@ -2557,7 +2557,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==3.10.10 +python-roborock==3.12.2 # homeassistant.components.smarttub python-smarttub==0.0.45 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82a4e8e68b4e63..ce513b1e4cc0b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -197,7 +197,7 @@ aioaquacell==0.2.0 aioaseko==1.0.0 # homeassistant.components.asuswrt -aioasuswrt==1.5.1 +aioasuswrt==1.5.2 # homeassistant.components.husqvarna_automower aioautomower==2.7.1 @@ -504,7 +504,7 @@ aranet4==2.5.1 arcam-fmj==1.8.2 # homeassistant.components.asuswrt -asusrouter==1.21.1 +asusrouter==1.21.3 # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms @@ -585,7 +585,7 @@ bleak==1.0.1 blebox-uniapi==2.5.0 # homeassistant.components.blink -blinkpy==0.24.1 +blinkpy==0.25.1 # homeassistant.components.blue_current bluecurrent-api==1.3.2 @@ -966,7 +966,7 @@ google-nest-sdm==9.1.2 google-photos-library-api==0.12.1 # homeassistant.components.google_air_quality -google_air_quality_api==2.0.0 +google_air_quality_api==2.0.2 # homeassistant.components.slide # homeassistant.components.slide_local @@ -1024,7 +1024,7 @@ habiticalib==0.4.6 habluetooth==5.7.0 # homeassistant.components.hanna -hanna-cloud==0.0.6 +hanna-cloud==0.0.7 # homeassistant.components.cloud hass-nabucasa==1.7.0 @@ -1086,7 +1086,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==11.1.0 +ical==12.1.1 # homeassistant.components.caldav icalendar==6.3.1 @@ -1801,7 +1801,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.2.2 +pylamarzocco==2.2.4 # homeassistant.components.lastfm pylast==5.1.0 @@ -1920,7 +1920,7 @@ pypalazzetti==0.1.20 pypaperless==4.1.1 # homeassistant.components.lcn -pypck==0.9.5 +pypck==0.9.7 # homeassistant.components.pglab pypglab==0.0.5 @@ -2014,7 +2014,7 @@ pysmappee==0.2.29 pysmarlaapi==0.9.2 # homeassistant.components.smartthings -pysmartthings==3.5.0 +pysmartthings==3.5.1 # homeassistant.components.smarty pysmarty2==0.10.3 @@ -2135,7 +2135,7 @@ python-pooldose==0.7.8 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==3.10.10 +python-roborock==3.12.2 # homeassistant.components.smarttub python-smarttub==0.0.45 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index cc89285302ac87..e61021acc4d109 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -214,6 +214,9 @@ # Pin pytest-rerunfailures to prevent accidental breaks pytest-rerunfailures==16.0.1 + +# pycares 5.x is not yet compatible with aiodns +pycares==4.11.0 """ GENERATED_MESSAGE = ( diff --git a/tests/components/blink/conftest.py b/tests/components/blink/conftest.py index 7c46d13437bba8..1a7f4f6f2cd460 100644 --- a/tests/components/blink/conftest.py +++ b/tests/components/blink/conftest.py @@ -85,7 +85,7 @@ def mock_config_fixture(): data={ CONF_USERNAME: "test_user", CONF_PASSWORD: "Password", - "device_id": "Home Assistant", + "hardware_id": "Home Assistant", "uid": "BlinkCamera_e1233333e2-0909-09cd-777a-123456789012", "token": "A_token", "unique_id": "an_email@email.com", @@ -95,5 +95,5 @@ def mock_config_fixture(): "account_id": 654321, }, entry_id=str(uuid4()), - version=3, + version=4, ) diff --git a/tests/components/blink/snapshots/test_diagnostics.ambr b/tests/components/blink/snapshots/test_diagnostics.ambr index 54df2b48cdbbca..bf2d38ac473843 100644 --- a/tests/components/blink/snapshots/test_diagnostics.ambr +++ b/tests/components/blink/snapshots/test_diagnostics.ambr @@ -28,7 +28,7 @@ 'data': dict({ 'account_id': 654321, 'client_id': 123456, - 'device_id': 'Home Assistant', + 'hardware_id': 'Home Assistant', 'host': 'u034.immedia-semi.com', 'password': '**REDACTED**', 'region_id': 'u034', @@ -52,7 +52,7 @@ ]), 'title': 'Mock Title', 'unique_id': None, - 'version': 3, + 'version': 4, }), }) # --- diff --git a/tests/components/blink/test_init.py b/tests/components/blink/test_init.py index a4629a9b461fdd..5c30e575b5ea9a 100644 --- a/tests/components/blink/test_init.py +++ b/tests/components/blink/test_init.py @@ -113,3 +113,32 @@ async def test_migrate( await hass.async_block_till_done() entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) assert entry.state is ConfigEntryState.MIGRATION_ERROR + + +async def test_migrate_v3_to_v4( + hass: HomeAssistant, + mock_blink_api: MagicMock, + mock_blink_auth_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test migration from version 3 to 4 (device_id to hardware_id).""" + mock_config_entry.add_to_hass(hass) + + # Set up v3 config entry with device_id + data = {**mock_config_entry.data} + data.pop("hardware_id", None) + data["device_id"] = "Home Assistant" + hass.config_entries.async_update_entry( + mock_config_entry, + version=3, + data=data, + ) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert entry.state is ConfigEntryState.LOADED + assert entry.version == 4 + assert "hardware_id" in entry.data + assert "device_id" not in entry.data + assert entry.data["hardware_id"] == "Home Assistant" diff --git a/tests/components/growatt_server/snapshots/test_sensor.ambr b/tests/components/growatt_server/snapshots/test_sensor.ambr index 6a39175de957d2..226821e39f09e7 100644 --- a/tests/components/growatt_server/snapshots/test_sensor.ambr +++ b/tests/components/growatt_server/snapshots/test_sensor.ambr @@ -229,7 +229,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -268,6 +270,7 @@ 'device_class': 'power', 'friendly_name': 'MIN123456 Battery 1 charging W', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -283,7 +286,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -322,6 +327,7 @@ 'device_class': 'power', 'friendly_name': 'MIN123456 Battery 1 discharging W', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -337,7 +343,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -376,6 +384,7 @@ 'device_class': 'power', 'friendly_name': 'MIN123456 Battery 2 charging W', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -391,7 +400,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -430,6 +441,7 @@ 'device_class': 'power', 'friendly_name': 'MIN123456 Battery 2 discharging W', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -730,7 +742,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -769,6 +783,7 @@ 'device_class': 'power', 'friendly_name': 'MIN123456 Export power', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -898,7 +913,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -937,6 +954,7 @@ 'device_class': 'power', 'friendly_name': 'MIN123456 Import power', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -1060,7 +1078,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -1099,6 +1119,7 @@ 'device_class': 'power', 'friendly_name': 'MIN123456 Input 1 wattage', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -1222,7 +1243,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -1261,6 +1284,7 @@ 'device_class': 'power', 'friendly_name': 'MIN123456 Input 2 wattage', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -1384,7 +1408,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -1423,6 +1449,7 @@ 'device_class': 'power', 'friendly_name': 'MIN123456 Input 3 wattage', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -1546,7 +1573,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -1585,6 +1614,7 @@ 'device_class': 'power', 'friendly_name': 'MIN123456 Input 4 wattage', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -1600,7 +1630,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -1639,6 +1671,7 @@ 'device_class': 'power', 'friendly_name': 'MIN123456 Internal wattage', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -2737,7 +2770,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -2776,6 +2811,7 @@ 'device_class': 'power', 'friendly_name': 'MIN123456 Local load power', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -2791,7 +2827,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -2830,6 +2868,7 @@ 'device_class': 'power', 'friendly_name': 'MIN123456 Output power', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -2956,7 +2995,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -2995,6 +3036,7 @@ 'device_class': 'power', 'friendly_name': 'MIN123456 Self power', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -3118,7 +3160,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -3157,6 +3201,7 @@ 'device_class': 'power', 'friendly_name': 'MIN123456 System power', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -3613,7 +3658,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -3652,6 +3699,7 @@ 'device_class': 'power', 'friendly_name': 'Test Plant Total Maximum power', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -3936,7 +3984,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -3975,6 +4025,7 @@ 'device_class': 'power', 'friendly_name': 'Test Plant Total Maximum power', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -4372,7 +4423,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -4411,6 +4464,7 @@ 'device_class': 'power', 'friendly_name': 'TLX123456 Battery 1 charging W', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -4426,7 +4480,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -4465,6 +4521,7 @@ 'device_class': 'power', 'friendly_name': 'TLX123456 Battery 1 discharging W', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -4480,7 +4537,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -4519,6 +4578,7 @@ 'device_class': 'power', 'friendly_name': 'TLX123456 Battery 2 charging W', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -4534,7 +4594,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -4573,6 +4635,7 @@ 'device_class': 'power', 'friendly_name': 'TLX123456 Battery 2 discharging W', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -4873,7 +4936,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -4912,6 +4977,7 @@ 'device_class': 'power', 'friendly_name': 'TLX123456 Export power', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -5041,7 +5107,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -5080,6 +5148,7 @@ 'device_class': 'power', 'friendly_name': 'TLX123456 Import power', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -5203,7 +5272,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -5242,6 +5313,7 @@ 'device_class': 'power', 'friendly_name': 'TLX123456 Input 1 wattage', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -5365,7 +5437,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -5404,6 +5478,7 @@ 'device_class': 'power', 'friendly_name': 'TLX123456 Input 2 wattage', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -5527,7 +5602,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -5566,6 +5643,7 @@ 'device_class': 'power', 'friendly_name': 'TLX123456 Input 3 wattage', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -5689,7 +5767,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -5728,6 +5808,7 @@ 'device_class': 'power', 'friendly_name': 'TLX123456 Input 4 wattage', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -5743,7 +5824,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -5782,6 +5865,7 @@ 'device_class': 'power', 'friendly_name': 'TLX123456 Internal wattage', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -6880,7 +6964,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -6919,6 +7005,7 @@ 'device_class': 'power', 'friendly_name': 'TLX123456 Local load power', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -6934,7 +7021,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -6973,6 +7062,7 @@ 'device_class': 'power', 'friendly_name': 'TLX123456 Output power', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -7099,7 +7189,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -7138,6 +7230,7 @@ 'device_class': 'power', 'friendly_name': 'TLX123456 Self power', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -7261,7 +7354,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -7300,6 +7395,7 @@ 'device_class': 'power', 'friendly_name': 'TLX123456 System power', 'icon': 'mdi:solar-power', + 'state_class': , 'unit_of_measurement': , }), 'context': , diff --git a/tests/components/matter/snapshots/test_select.ambr b/tests/components/matter/snapshots/test_select.ambr index d0bc46f2268533..00f51698a37e33 100644 --- a/tests/components/matter/snapshots/test_select.ambr +++ b/tests/components/matter/snapshots/test_select.ambr @@ -241,10 +241,7 @@ 'capabilities': dict({ 'options': list([ 'normal', - 'vacation', - 'privacy', 'no_remote_lock_unlock', - 'passage', ]), }), 'config_entry_id': , @@ -282,10 +279,7 @@ 'friendly_name': 'Aqara Smart Lock U200 Operating mode', 'options': list([ 'normal', - 'vacation', - 'privacy', 'no_remote_lock_unlock', - 'passage', ]), }), 'context': , @@ -684,10 +678,7 @@ 'capabilities': dict({ 'options': list([ 'normal', - 'vacation', - 'privacy', 'no_remote_lock_unlock', - 'passage', ]), }), 'config_entry_id': , @@ -725,10 +716,7 @@ 'friendly_name': 'Mock Door Lock Operating mode', 'options': list([ 'normal', - 'vacation', - 'privacy', 'no_remote_lock_unlock', - 'passage', ]), }), 'context': , @@ -869,10 +857,7 @@ 'capabilities': dict({ 'options': list([ 'normal', - 'vacation', - 'privacy', 'no_remote_lock_unlock', - 'passage', ]), }), 'config_entry_id': , @@ -910,10 +895,7 @@ 'friendly_name': 'Mock Door Lock with unbolt Operating mode', 'options': list([ 'normal', - 'vacation', - 'privacy', 'no_remote_lock_unlock', - 'passage', ]), }), 'context': , @@ -2454,10 +2436,7 @@ 'capabilities': dict({ 'options': list([ 'normal', - 'vacation', - 'privacy', 'no_remote_lock_unlock', - 'passage', ]), }), 'config_entry_id': , @@ -2495,10 +2474,7 @@ 'friendly_name': 'Mock Lock Operating mode', 'options': list([ 'normal', - 'vacation', - 'privacy', 'no_remote_lock_unlock', - 'passage', ]), }), 'context': , @@ -3657,10 +3633,8 @@ 'capabilities': dict({ 'options': list([ 'normal', - 'vacation', 'privacy', 'no_remote_lock_unlock', - 'passage', ]), }), 'config_entry_id': , @@ -3698,10 +3672,8 @@ 'friendly_name': 'Secuyou Smart Lock Operating mode', 'options': list([ 'normal', - 'vacation', 'privacy', 'no_remote_lock_unlock', - 'passage', ]), }), 'context': , diff --git a/tests/components/matter/test_select.py b/tests/components/matter/test_select.py index 64fd5a98816a98..f2280633b4e42c 100644 --- a/tests/components/matter/test_select.py +++ b/tests/components/matter/test_select.py @@ -8,7 +8,6 @@ import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.matter.select import DOOR_LOCK_OPERATING_MODE_MAP from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -314,22 +313,30 @@ async def test_door_lock_operating_mode_select( """Test Door Lock Operating Mode select entity discovery and interaction. Verifies: - - Options match mapping in DOOR_LOCK_OPERATING_MODE_MAP + - Options are filtered based on SupportedOperatingModes bitmap - Attribute updates reflect current option - Selecting an option writes correct enum value """ entity_id = "select.secuyou_smart_lock_operating_mode" state = hass.states.get(entity_id) assert state, "Missing operating mode select entity" - assert state.attributes["options"] == list(DOOR_LOCK_OPERATING_MODE_MAP.values()) - # Initial state should be one of the allowed options + # According to the spec, bit=0 means supported and bit=1 means not supported. + # The fixture bitmap clears bits 0, 2, and 3, so the supported modes are + # Normal, Privacy, and NoRemoteLockUnlock; the other bits are set (not + # supported). + assert set(state.attributes["options"]) == { + "normal", + "privacy", + "no_remote_lock_unlock", + } + # Verify that the initial state is part of the allowed options assert state.state in state.attributes["options"] # Dynamically obtain ids instead of hardcoding door_lock_cluster_id = clusters.DoorLock.Attributes.OperatingMode.cluster_id operating_mode_attr_id = clusters.DoorLock.Attributes.OperatingMode.attribute_id - # Change OperatingMode attribute on the node to 'privacy' + # Change OperatingMode attribute on the node to a supported mode ('privacy') set_node_attribute( matter_node, 1, @@ -341,12 +348,12 @@ async def test_door_lock_operating_mode_select( state = hass.states.get(entity_id) assert state.state == "privacy" - # Select another option (vacation) via service to validate mapping + # Select another supported option (NoRemoteLockUnlock) via service to validate mapping matter_client.write_attribute.reset_mock() await hass.services.async_call( "select", "select_option", - {"entity_id": entity_id, "option": "vacation"}, + {"entity_id": entity_id, "option": "no_remote_lock_unlock"}, blocking=True, ) assert matter_client.write_attribute.call_count == 1 @@ -356,5 +363,5 @@ async def test_door_lock_operating_mode_select( endpoint_id=1, attribute=clusters.DoorLock.Attributes.OperatingMode, ), - value=clusters.DoorLock.Enums.OperatingModeEnum.kVacation, + value=clusters.DoorLock.Enums.OperatingModeEnum.kNoRemoteLockUnlock, ) diff --git a/tests/components/ohme/snapshots/test_sensor.ambr b/tests/components/ohme/snapshots/test_sensor.ambr index f074aa8e6bdf33..7e7758c58cd101 100644 --- a/tests/components/ohme/snapshots/test_sensor.ambr +++ b/tests/components/ohme/snapshots/test_sensor.ambr @@ -52,7 +52,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -90,6 +92,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'current', 'friendly_name': 'Ohme Home Pro Current', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -164,7 +167,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -205,6 +210,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'power', 'friendly_name': 'Ohme Home Pro Power', + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -286,7 +292,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'config_subentry_id': , 'device_class': None, @@ -324,6 +332,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'battery', 'friendly_name': 'Ohme Home Pro Vehicle battery', + 'state_class': , 'unit_of_measurement': '%', }), 'context': , diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index 17637b3bc4d6c9..aaf9a69e112273 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -21,6 +21,7 @@ NetworkInfo, RoborockBase, RoborockDyadStateCode, + ValleyElectricityTimer, ZeoError, ZeoState, ) @@ -40,6 +41,9 @@ from roborock.devices.traits.v1.routines import RoutinesTrait from roborock.devices.traits.v1.smart_wash_params import SmartWashParamsTrait from roborock.devices.traits.v1.status import StatusTrait +from roborock.devices.traits.v1.valley_electricity_timer import ( + ValleyElectricityTimerTrait, +) from roborock.devices.traits.v1.volume import SoundVolumeTrait from roborock.devices.traits.v1.wash_towel_mode import WashTowelModeTrait from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol @@ -68,6 +72,7 @@ STATUS, USER_DATA, USER_EMAIL, + VALLEY_ELECTRICITY_TIMER, ) from tests.common import MockConfigEntry @@ -188,6 +193,25 @@ async def set_dnd_timer(timer: DnDTimer) -> None: return dnd_trait +def make_valley_electric_timer(dataclass_template: RoborockBase) -> AsyncMock: + """Make a function for the fake timer trait that emulates the real behavior.""" + valley_electric_timer_trait = make_mock_switch( + trait_spec=ValleyElectricityTimerTrait, + dataclass_template=dataclass_template, + ) + + async def set_timer(timer: ValleyElectricityTimer) -> None: + setattr(valley_electric_timer_trait, "start_hour", timer.start_hour) + setattr(valley_electric_timer_trait, "start_minute", timer.start_minute) + setattr(valley_electric_timer_trait, "end_hour", timer.end_hour) + setattr(valley_electric_timer_trait, "end_minute", timer.end_minute) + setattr(valley_electric_timer_trait, "enabled", timer.enabled) + + valley_electric_timer_trait.set_timer = AsyncMock() + valley_electric_timer_trait.set_timer.side_effect = set_timer + return valley_electric_timer_trait + + def make_home_trait( map_info: list[MultiMapsListMapInfo], current_map: int | None, @@ -257,7 +281,9 @@ def create_v1_properties(network_info: NetworkInfo) -> AsyncMock: v1_properties.child_lock = make_mock_switch() v1_properties.led_status = make_mock_switch() v1_properties.flow_led_status = make_mock_switch() - v1_properties.valley_electricity_timer = make_mock_switch() + v1_properties.valley_electricity_timer = make_valley_electric_timer( + dataclass_template=VALLEY_ELECTRICITY_TIMER, + ) v1_properties.dust_collection_mode = make_mock_trait( trait_spec=DustCollectionModeTrait ) diff --git a/tests/components/roborock/mock_data.py b/tests/components/roborock/mock_data.py index f8407e0b0e37f1..c9cd219e35bf3b 100644 --- a/tests/components/roborock/mock_data.py +++ b/tests/components/roborock/mock_data.py @@ -14,6 +14,7 @@ NetworkInfo, S7Status, UserData, + ValleyElectricityTimer, ) from vacuum_map_parser_base.config.image_config import ImageConfig from vacuum_map_parser_base.map_data import ImageData @@ -1072,6 +1073,16 @@ } ) +VALLEY_ELECTRICITY_TIMER = ValleyElectricityTimer.from_dict( + { + "start_hour": 23, + "start_minute": 0, + "end_hour": 7, + "end_minute": 0, + "enabled": 1, + } +) + STATUS = S7Status.from_dict( { "msg_ver": 2, diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index 4773badbe14ffb..b2330ae793ad66 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -10,6 +10,7 @@ RoborockInvalidUserAgreement, RoborockNoUserAgreement, ) +from roborock.exceptions import RoborockException from homeassistant.components.roborock.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -330,3 +331,71 @@ async def test_cloud_api_repair( await hass.async_block_till_done() assert len(issue_registry.issues) == 0 + + +@pytest.mark.parametrize("platforms", [[Platform.SENSOR]]) +async def test_zeo_device_fails_setup( + hass: HomeAssistant, + mock_roborock_entry: MockConfigEntry, + device_registry: DeviceRegistry, + fake_devices: list[FakeDevice], +) -> None: + """Simulate an error while setting up a zeo device.""" + # We have a single zeo device in the test setup. Find it then set it to fail. + zeo_device = next( + (device for device in fake_devices if device.zeo is not None), + None, + ) + assert zeo_device is not None + zeo_device.zeo.query_values.side_effect = RoborockException("Simulated Zeo failure") + + await hass.config_entries.async_setup(mock_roborock_entry.entry_id) + assert mock_roborock_entry.state is ConfigEntryState.LOADED + + # The current behavior is that we do not add the Zeo device if it fails to setup + found_devices = device_registry.devices.get_devices_for_config_entry_id( + mock_roborock_entry.entry_id + ) + assert {device.name for device in found_devices} == { + "Roborock S7 MaxV", + "Roborock S7 MaxV Dock", + "Roborock S7 2", + "Roborock S7 2 Dock", + "Dyad Pro", + # Zeo device is missing + } + + +@pytest.mark.parametrize("platforms", [[Platform.SENSOR]]) +async def test_dyad_device_fails_setup( + hass: HomeAssistant, + mock_roborock_entry: MockConfigEntry, + device_registry: DeviceRegistry, + fake_devices: list[FakeDevice], +) -> None: + """Simulate an error while setting up a dyad device.""" + # We have a single dyad device in the test setup. Find it then set it to fail. + dyad_device = next( + (device for device in fake_devices if device.dyad is not None), + None, + ) + assert dyad_device is not None + dyad_device.dyad.query_values.side_effect = RoborockException( + "Simulated Dyad failure" + ) + + await hass.config_entries.async_setup(mock_roborock_entry.entry_id) + assert mock_roborock_entry.state is ConfigEntryState.LOADED + + # The current behavior is that we do not add the Dyad device if it fails to setup + found_devices = device_registry.devices.get_devices_for_config_entry_id( + mock_roborock_entry.entry_id + ) + assert {device.name for device in found_devices} == { + "Roborock S7 MaxV", + "Roborock S7 MaxV Dock", + "Roborock S7 2", + "Roborock S7 2 Dock", + # Dyad device is missing + "Zeo One", + } diff --git a/tests/components/roborock/test_time.py b/tests/components/roborock/test_time.py index 4bad15aa6817fa..01a2499ea791d2 100644 --- a/tests/components/roborock/test_time.py +++ b/tests/components/roborock/test_time.py @@ -1,10 +1,12 @@ """Test Roborock Time platform.""" +from collections.abc import Callable from datetime import time +from typing import Any import pytest import roborock -from roborock.data import DnDTimer +from roborock.data import DnDTimer, RoborockBaseTimer, ValleyElectricityTimer from homeassistant.components.time import SERVICE_SET_VALUE from homeassistant.const import Platform @@ -22,23 +24,44 @@ def platforms() -> list[Platform]: return [Platform.TIME] +@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( - ("entity_id", "start_state", "expected_args", "end_state"), + ("entity_id", "start_state", "expected_call", "expected_args", "end_state"), [ ( "time.roborock_s7_maxv_do_not_disturb_begin", "22:00:00", + lambda x: x.v1_properties.dnd.set_dnd_timer, DnDTimer(start_hour=1, start_minute=1, end_hour=7, end_minute=0, enabled=1), "01:01:00", ), ( "time.roborock_s7_maxv_do_not_disturb_end", "07:00:00", + lambda x: x.v1_properties.dnd.set_dnd_timer, DnDTimer( start_hour=22, start_minute=0, end_hour=1, end_minute=1, enabled=1 ), "01:01:00", ), + ( + "time.roborock_s7_maxv_off_peak_start", + "23:00:00", + lambda x: x.v1_properties.valley_electricity_timer.set_timer, + ValleyElectricityTimer( + start_hour=1, start_minute=1, end_hour=7, end_minute=0, enabled=1 + ), + "01:01:00", + ), + ( + "time.roborock_s7_maxv_off_peak_end", + "07:00:00", + lambda x: x.v1_properties.valley_electricity_timer.set_timer, + ValleyElectricityTimer( + start_hour=23, start_minute=0, end_hour=1, end_minute=1, enabled=1 + ), + "01:01:00", + ), ], ) async def test_update_success( @@ -48,7 +71,8 @@ async def test_update_success( entity_id: str, start_state: str, end_state: str, - expected_args: DnDTimer, + expected_call: Callable[[FakeDevice], Any], + expected_args: RoborockBaseTimer, ) -> None: """Test turning switch entities on and off.""" # Ensure that the entity exist, as these test can pass even if there is no entity. @@ -64,10 +88,11 @@ async def test_update_success( target={"entity_id": entity_id}, ) - assert fake_vacuum.v1_properties.dnd.set_dnd_timer.call_count == 1 + call = expected_call(fake_vacuum) + assert call.call_count == 1 # Since we update the begin or end time separately: Verify that the args are built properly # by reading the existing value and only updating the relevant fields. - assert fake_vacuum.v1_properties.dnd.set_dnd_timer.call_args == ((expected_args,),) + assert call.call_args == ((expected_args,),) state = hass.states.get(entity_id) assert state is not None diff --git a/tests/components/tuya/conftest.py b/tests/components/tuya/conftest.py index 30a76ba3809243..d74fae6be7d98c 100644 --- a/tests/components/tuya/conftest.py +++ b/tests/components/tuya/conftest.py @@ -176,6 +176,7 @@ async def _create_device(hass: HomeAssistant, mock_device_code: str) -> Customer if device.update_time: device.update_time = int(dt_util.as_timestamp(device.update_time)) device.support_local = details.get("support_local") + device.local_strategy = details.get("local_strategy") device.mqtt_connected = details.get("mqtt_connected") device.function = { diff --git a/tests/components/tuya/snapshots/test_climate.ambr b/tests/components/tuya/snapshots/test_climate.ambr index 542c0a5c8f5147..cf98172a021169 100644 --- a/tests/components/tuya/snapshots/test_climate.ambr +++ b/tests/components/tuya/snapshots/test_climate.ambr @@ -651,7 +651,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }) # --- # name: test_platform_setup_and_discovery[climate.itc_308_wifi_thermostat-entry] @@ -713,7 +713,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }) # --- # name: test_platform_setup_and_discovery[climate.kabinet-entry] @@ -1007,7 +1007,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }) # --- # name: test_platform_setup_and_discovery[climate.polotentsosushitel-entry] diff --git a/tests/components/tuya/snapshots/test_diagnostics.ambr b/tests/components/tuya/snapshots/test_diagnostics.ambr index 54e31002f16e90..5901af7c8478f6 100644 --- a/tests/components/tuya/snapshots/test_diagnostics.ambr +++ b/tests/components/tuya/snapshots/test_diagnostics.ambr @@ -198,6 +198,7 @@ 'name_by_user': None, }), 'id': '2pxfek1jjrtctiyglam', + 'local_strategy': None, 'mqtt_connected': True, 'name': 'Multifunction alarm', 'online': True, @@ -369,6 +370,7 @@ 'name_by_user': None, }), 'id': 'cwwk68dyfsh2eqi4jbqr', + 'local_strategy': None, 'mqtt_connected': True, 'name': 'Gas sensor', 'online': True, @@ -512,6 +514,7 @@ 'name_by_user': None, }), 'id': 'vrhdtr5fawoiyth9qdt', + 'local_strategy': None, 'mqtt_connected': True, 'name': 'Framboisiers', 'online': True, @@ -644,6 +647,7 @@ 'name_by_user': None, }), 'id': 'cwwk68dyfsh2eqi4jbqr', + 'local_strategy': None, 'name': 'Gas sensor', 'online': True, 'product_id': '4iqe2hsfyd86kwwc', diff --git a/tests/components/webhook/test_trigger.py b/tests/components/webhook/test_trigger.py index 74a2d15b9ba948..2f7f2900771325 100644 --- a/tests/components/webhook/test_trigger.py +++ b/tests/components/webhook/test_trigger.py @@ -8,6 +8,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component +from tests.common import async_capture_events from tests.typing import ClientSessionGenerator @@ -377,3 +378,46 @@ def store_event(event): assert len(events) == 1 assert events[0].data["hello"] == "yo world" + + +async def test_webhook_query_json_header_no_payload( + hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator +) -> None: + """Test requests with application/json header but no payload.""" + events = async_capture_events(hass, "test_success") + + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "trigger": { + "platform": "webhook", + "webhook_id": "no_payload_webhook", + "local_only": True, + "allowed_methods": ["GET", "POST"], + }, + "action": { + "event": "test_success", + }, + } + }, + ) + await hass.async_block_till_done() + client = await hass_client_no_auth() + + # GET + response = await client.get( + "/api/webhook/no_payload_webhook", headers={"Content-Type": "application/json"} + ) + await hass.async_block_till_done() + assert response.status == 200 + + # POST + response = await client.post( + "/api/webhook/no_payload_webhook", headers={"Content-Type": "application/json"} + ) + await hass.async_block_till_done() + assert response.status == 200 + + assert len(events) == 2 diff --git a/tests/components/xbox/test_image.py b/tests/components/xbox/test_image.py index 96fc978f1d09fa..7e04192636f34f 100644 --- a/tests/components/xbox/test_image.py +++ b/tests/components/xbox/test_image.py @@ -115,12 +115,12 @@ async def test_load_image_from_url( "rgWHJigthrlsHCxEOMG9UGNdojCYasYt6MJHBjmxmtuAHJeo.sOkUiPmg4JHXvOS82c3UOrvdJTDaCKwCwHPJ0t0Plha8oHFC1i_o-&format=png" ).respond(status_code=HTTPStatus.OK, content_type="image/png", content=b"Test2") - freezer.tick(timedelta(seconds=10)) + freezer.tick(timedelta(seconds=15)) async_fire_time_changed(hass) await hass.async_block_till_done() assert (state := hass.states.get("image.gsr_ae_gamerpic")) - assert state.state == "2025-06-16T00:00:10+00:00" + assert state.state == "2025-06-16T00:00:15+00:00" access_token = state.attributes["access_token"] assert (