This commit is contained in:
Franck Nijhof
2025-12-12 19:10:33 +01:00
committed by GitHub
54 changed files with 599 additions and 176 deletions
@@ -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"]
}
@@ -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
@@ -21,7 +21,7 @@ from homeassistant.core import callback
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 @@ class BlinkConfigFlow(ConfigFlow, domain=DOMAIN):
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),
)
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"]
}
@@ -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"]
}
@@ -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"]
}
@@ -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",
@@ -27,6 +27,7 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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(
@@ -79,6 +79,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv1",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -122,6 +123,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv2",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -165,6 +167,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv3",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -208,6 +211,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv4",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -234,6 +238,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="ppv",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -258,6 +263,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pac",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -323,6 +329,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
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 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pacToLocalLoad",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -453,6 +464,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pacToUserTotal",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -461,6 +473,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pacToGridTotal",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -545,6 +558,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="psystem",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
GrowattSensorEntityDescription(
@@ -553,6 +567,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="pself",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
precision=1,
),
)
@@ -50,5 +50,6 @@ TOTAL_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
api_key="nominalPower",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
)
+1 -1
View File
@@ -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"]
}
+7
View File
@@ -39,6 +39,10 @@ if TYPE_CHECKING:
_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(
+3 -3
View File
@@ -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"
}
},
@@ -37,5 +37,5 @@
"iot_class": "cloud_push",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==2.2.2"]
"requirements": ["pylamarzocco==2.2.4"]
}
+1 -1
View File
@@ -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"]
}
@@ -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"]
}
@@ -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"]
}
+49 -4
View File
@@ -183,6 +183,48 @@ class MatterModeSelectEntity(MatterAttributeSelectEntity):
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 @@ DISCOVERY_SCHEMAS = [
),
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,
),
),
]
+1 -1
View File
@@ -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.",
+3
View File
@@ -49,6 +49,7 @@ SENSORS = [
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 @@ SENSORS = [
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 @@ SENSORS = [
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
suggested_display_precision=0,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda client: client.battery,
),
OhmeSensorDescription(
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["ical"],
"quality_scale": "silver",
"requirements": ["ical==11.1.0"]
"requirements": ["ical==12.1.1"]
}
@@ -394,7 +394,14 @@ class RoborockWashingMachineUpdateCoordinator(
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 @@ class RoborockWetDryVacUpdateCoordinator(
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
@@ -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"
]
}
+17 -15
View File
@@ -7,7 +7,7 @@ from datetime import time
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 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [
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 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [
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
@@ -31,5 +31,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"quality_scale": "bronze",
"requirements": ["pysmartthings==3.5.0"]
"requirements": ["pysmartthings==3.5.1"]
}
@@ -147,16 +147,16 @@ class SwitchBotCloudBinarySensor(SwitchBotCloudEntity, BinarySensorEntity):
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
)
+12 -12
View File
@@ -433,23 +433,23 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
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:
@@ -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),
}
+2 -2
View File
@@ -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"],
)
+4 -1
View File
@@ -15,6 +15,7 @@ from homeassistant.helpers import config_validation as cv
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()
+12 -4
View File
@@ -81,10 +81,11 @@ class XboxUpdateCoordinator(DataUpdateCoordinator[XboxData]):
_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 @@ class XboxUpdateCoordinator(DataUpdateCoordinator[XboxData]):
)
# retrieve title details
title_data: dict[str, Title] = {}
for person in presence_data.values():
if presence_detail := next(
(
@@ -227,6 +227,12 @@ class XboxUpdateCoordinator(DataUpdateCoordinator[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 @@ class XboxUpdateCoordinator(DataUpdateCoordinator[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 @@ class XboxUpdateCoordinator(DataUpdateCoordinator[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."""
+1 -1
View File
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
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)
+3
View File
@@ -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
+1 -1
View File
@@ -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."
+10 -10
View File
@@ -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
+10 -10
View File
@@ -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
+3
View File
@@ -214,6 +214,9 @@ 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
"""
GENERATED_MESSAGE = (
+2 -2
View File
@@ -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,
)
@@ -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,
}),
})
# ---
+29
View File
@@ -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"
@@ -229,7 +229,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -268,6 +270,7 @@
'device_class': 'power',
'friendly_name': 'MIN123456 Battery 1 charging W',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -283,7 +286,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -322,6 +327,7 @@
'device_class': 'power',
'friendly_name': 'MIN123456 Battery 1 discharging W',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -337,7 +343,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -376,6 +384,7 @@
'device_class': 'power',
'friendly_name': 'MIN123456 Battery 2 charging W',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -391,7 +400,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -430,6 +441,7 @@
'device_class': 'power',
'friendly_name': 'MIN123456 Battery 2 discharging W',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -730,7 +742,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -769,6 +783,7 @@
'device_class': 'power',
'friendly_name': 'MIN123456 Export power',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -898,7 +913,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -937,6 +954,7 @@
'device_class': 'power',
'friendly_name': 'MIN123456 Import power',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -1060,7 +1078,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -1099,6 +1119,7 @@
'device_class': 'power',
'friendly_name': 'MIN123456 Input 1 wattage',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -1222,7 +1243,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -1261,6 +1284,7 @@
'device_class': 'power',
'friendly_name': 'MIN123456 Input 2 wattage',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -1384,7 +1408,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -1423,6 +1449,7 @@
'device_class': 'power',
'friendly_name': 'MIN123456 Input 3 wattage',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -1546,7 +1573,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -1585,6 +1614,7 @@
'device_class': 'power',
'friendly_name': 'MIN123456 Input 4 wattage',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -1600,7 +1630,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -1639,6 +1671,7 @@
'device_class': 'power',
'friendly_name': 'MIN123456 Internal wattage',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -2737,7 +2770,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -2776,6 +2811,7 @@
'device_class': 'power',
'friendly_name': 'MIN123456 Local load power',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -2791,7 +2827,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -2830,6 +2868,7 @@
'device_class': 'power',
'friendly_name': 'MIN123456 Output power',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -2956,7 +2995,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -2995,6 +3036,7 @@
'device_class': 'power',
'friendly_name': 'MIN123456 Self power',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -3118,7 +3160,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -3157,6 +3201,7 @@
'device_class': 'power',
'friendly_name': 'MIN123456 System power',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -3613,7 +3658,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -3652,6 +3699,7 @@
'device_class': 'power',
'friendly_name': 'Test Plant Total Maximum power',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -3936,7 +3984,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -3975,6 +4025,7 @@
'device_class': 'power',
'friendly_name': 'Test Plant Total Maximum power',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -4372,7 +4423,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -4411,6 +4464,7 @@
'device_class': 'power',
'friendly_name': 'TLX123456 Battery 1 charging W',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -4426,7 +4480,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -4465,6 +4521,7 @@
'device_class': 'power',
'friendly_name': 'TLX123456 Battery 1 discharging W',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -4480,7 +4537,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -4519,6 +4578,7 @@
'device_class': 'power',
'friendly_name': 'TLX123456 Battery 2 charging W',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -4534,7 +4594,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -4573,6 +4635,7 @@
'device_class': 'power',
'friendly_name': 'TLX123456 Battery 2 discharging W',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -4873,7 +4936,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -4912,6 +4977,7 @@
'device_class': 'power',
'friendly_name': 'TLX123456 Export power',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -5041,7 +5107,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -5080,6 +5148,7 @@
'device_class': 'power',
'friendly_name': 'TLX123456 Import power',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -5203,7 +5272,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -5242,6 +5313,7 @@
'device_class': 'power',
'friendly_name': 'TLX123456 Input 1 wattage',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -5365,7 +5437,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -5404,6 +5478,7 @@
'device_class': 'power',
'friendly_name': 'TLX123456 Input 2 wattage',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -5527,7 +5602,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -5566,6 +5643,7 @@
'device_class': 'power',
'friendly_name': 'TLX123456 Input 3 wattage',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -5689,7 +5767,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -5728,6 +5808,7 @@
'device_class': 'power',
'friendly_name': 'TLX123456 Input 4 wattage',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -5743,7 +5824,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -5782,6 +5865,7 @@
'device_class': 'power',
'friendly_name': 'TLX123456 Internal wattage',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -6880,7 +6964,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -6919,6 +7005,7 @@
'device_class': 'power',
'friendly_name': 'TLX123456 Local load power',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -6934,7 +7021,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -6973,6 +7062,7 @@
'device_class': 'power',
'friendly_name': 'TLX123456 Output power',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -7099,7 +7189,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -7138,6 +7230,7 @@
'device_class': 'power',
'friendly_name': 'TLX123456 Self power',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -7261,7 +7354,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -7300,6 +7395,7 @@
'device_class': 'power',
'friendly_name': 'TLX123456 System power',
'icon': 'mdi:solar-power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
@@ -241,10 +241,7 @@
'capabilities': dict({
'options': list([
'normal',
'vacation',
'privacy',
'no_remote_lock_unlock',
'passage',
]),
}),
'config_entry_id': <ANY>,
@@ -282,10 +279,7 @@
'friendly_name': 'Aqara Smart Lock U200 Operating mode',
'options': list([
'normal',
'vacation',
'privacy',
'no_remote_lock_unlock',
'passage',
]),
}),
'context': <ANY>,
@@ -684,10 +678,7 @@
'capabilities': dict({
'options': list([
'normal',
'vacation',
'privacy',
'no_remote_lock_unlock',
'passage',
]),
}),
'config_entry_id': <ANY>,
@@ -725,10 +716,7 @@
'friendly_name': 'Mock Door Lock Operating mode',
'options': list([
'normal',
'vacation',
'privacy',
'no_remote_lock_unlock',
'passage',
]),
}),
'context': <ANY>,
@@ -869,10 +857,7 @@
'capabilities': dict({
'options': list([
'normal',
'vacation',
'privacy',
'no_remote_lock_unlock',
'passage',
]),
}),
'config_entry_id': <ANY>,
@@ -910,10 +895,7 @@
'friendly_name': 'Mock Door Lock with unbolt Operating mode',
'options': list([
'normal',
'vacation',
'privacy',
'no_remote_lock_unlock',
'passage',
]),
}),
'context': <ANY>,
@@ -2454,10 +2436,7 @@
'capabilities': dict({
'options': list([
'normal',
'vacation',
'privacy',
'no_remote_lock_unlock',
'passage',
]),
}),
'config_entry_id': <ANY>,
@@ -2495,10 +2474,7 @@
'friendly_name': 'Mock Lock Operating mode',
'options': list([
'normal',
'vacation',
'privacy',
'no_remote_lock_unlock',
'passage',
]),
}),
'context': <ANY>,
@@ -3657,10 +3633,8 @@
'capabilities': dict({
'options': list([
'normal',
'vacation',
'privacy',
'no_remote_lock_unlock',
'passage',
]),
}),
'config_entry_id': <ANY>,
@@ -3698,10 +3672,8 @@
'friendly_name': 'Secuyou Smart Lock Operating mode',
'options': list([
'normal',
'vacation',
'privacy',
'no_remote_lock_unlock',
'passage',
]),
}),
'context': <ANY>,
+15 -8
View File
@@ -8,7 +8,6 @@ from matter_server.common.helpers.util import create_attribute_path_from_attribu
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,
)
@@ -52,7 +52,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -90,6 +92,7 @@
'attributes': ReadOnlyDict({
'device_class': 'current',
'friendly_name': 'Ohme Home Pro Current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
}),
'context': <ANY>,
@@ -164,7 +167,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -205,6 +210,7 @@
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Ohme Home Pro Power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
'context': <ANY>,
@@ -286,7 +292,9 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -324,6 +332,7 @@
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Ohme Home Pro Vehicle battery',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
+27 -1
View File
@@ -21,6 +21,7 @@ from roborock.data import (
NetworkInfo,
RoborockBase,
RoborockDyadStateCode,
ValleyElectricityTimer,
ZeoError,
ZeoState,
)
@@ -40,6 +41,9 @@ from roborock.devices.traits.v1.network_info import NetworkInfoTrait
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 @@ from .mock_data import (
STATUS,
USER_DATA,
USER_EMAIL,
VALLEY_ELECTRICITY_TIMER,
)
from tests.common import MockConfigEntry
@@ -188,6 +193,25 @@ def make_dnd_timer(dataclass_template: RoborockBase) -> AsyncMock:
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
)
+11
View File
@@ -14,6 +14,7 @@ from roborock.data import (
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 @@ DND_TIMER = DnDTimer.from_dict(
}
)
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,
+69
View File
@@ -10,6 +10,7 @@ from roborock import (
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",
}
+30 -5
View File
@@ -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
+1
View File
@@ -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 = {
@@ -651,7 +651,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
'state': 'unknown',
})
# ---
# name: test_platform_setup_and_discovery[climate.itc_308_wifi_thermostat-entry]
@@ -713,7 +713,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
'state': 'unknown',
})
# ---
# name: test_platform_setup_and_discovery[climate.kabinet-entry]
@@ -1007,7 +1007,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
'state': 'unknown',
})
# ---
# name: test_platform_setup_and_discovery[climate.polotentsosushitel-entry]
@@ -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',
+44
View File
@@ -8,6 +8,7 @@ import pytest
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 @@ async def test_webhook_template(
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
+2 -2
View File
@@ -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 (