Merge branch 'dev' into whirlpool_sensor_door_remove

This commit is contained in:
Abílio Costa 2025-07-15 14:42:21 +01:00 committed by GitHub
commit f07ee5d972
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1413 additions and 1 deletions

View File

@ -29,6 +29,7 @@ PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.FAN,
Platform.LOCK,
Platform.SENSOR,
Platform.SWITCH,
@ -51,6 +52,7 @@ class SwitchbotDevices:
sensors: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
vacuums: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
locks: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
fans: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
@dataclass
@ -96,7 +98,6 @@ async def make_switchbot_devices(
for device in devices
]
)
return devices_data
@ -177,6 +178,16 @@ async def make_device_data(
else:
devices_data.switches.append((device, coordinator))
if isinstance(device, Device) and device.device_type in [
"Battery Circulator Fan",
"Circulator Fan",
]:
coordinator = await coordinator_for_device(
hass, entry, api, device, coordinators_by_id
)
devices_data.fans.append((device, coordinator))
devices_data.sensors.append((device, coordinator))
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up SwitchBot via API from a config entry."""

View File

@ -0,0 +1,120 @@
"""Support for the Switchbot Battery Circulator fan."""
import asyncio
from typing import Any
from switchbot_api import (
BatteryCirculatorFanCommands,
BatteryCirculatorFanMode,
CommonCommands,
)
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SwitchbotCloudData
from .const import DOMAIN
from .entity import SwitchBotCloudEntity
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up SwitchBot Cloud entry."""
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
async_add_entities(
SwitchBotCloudFan(data.api, device, coordinator)
for device, coordinator in data.devices.fans
)
class SwitchBotCloudFan(SwitchBotCloudEntity, FanEntity):
"""Representation of a SwitchBot Battery Circulator Fan."""
_attr_name = None
_attr_supported_features = (
FanEntityFeature.SET_SPEED
| FanEntityFeature.PRESET_MODE
| FanEntityFeature.TURN_OFF
| FanEntityFeature.TURN_ON
)
_attr_preset_modes = list(BatteryCirculatorFanMode)
_attr_is_on: bool | None = None
@property
def is_on(self) -> bool | None:
"""Return true if the entity is on."""
return self._attr_is_on
def _set_attributes(self) -> None:
"""Set attributes from coordinator data."""
if self.coordinator.data is None:
return
power: str = self.coordinator.data["power"]
mode: str = self.coordinator.data["mode"]
fan_speed: str = self.coordinator.data["fanSpeed"]
self._attr_is_on = power == "on"
self._attr_preset_mode = mode
self._attr_percentage = int(fan_speed)
self._attr_supported_features = (
FanEntityFeature.PRESET_MODE
| FanEntityFeature.TURN_OFF
| FanEntityFeature.TURN_ON
)
if self.is_on and self.preset_mode == BatteryCirculatorFanMode.DIRECT.value:
self._attr_supported_features |= FanEntityFeature.SET_SPEED
async def async_turn_on(
self,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Turn on the fan."""
await self.send_api_command(CommonCommands.ON)
await self.send_api_command(
command=BatteryCirculatorFanCommands.SET_WIND_MODE,
parameters=str(self.preset_mode),
)
if self.preset_mode == BatteryCirculatorFanMode.DIRECT.value:
await self.send_api_command(
command=BatteryCirculatorFanCommands.SET_WIND_SPEED,
parameters=str(self.percentage),
)
await asyncio.sleep(5)
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the fan."""
await self.send_api_command(CommonCommands.OFF)
await asyncio.sleep(5)
await self.coordinator.async_request_refresh()
async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage."""
await self.send_api_command(
command=BatteryCirculatorFanCommands.SET_WIND_MODE,
parameters=str(BatteryCirculatorFanMode.DIRECT.value),
)
await self.send_api_command(
command=BatteryCirculatorFanCommands.SET_WIND_SPEED,
parameters=str(percentage),
)
await asyncio.sleep(5)
await self.coordinator.async_request_refresh()
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
await self.send_api_command(
command=BatteryCirculatorFanCommands.SET_WIND_MODE,
parameters=preset_mode,
)
await asyncio.sleep(5)
await self.coordinator.async_request_refresh()

View File

@ -91,6 +91,7 @@ CO2_DESCRIPTION = SensorEntityDescription(
SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
"Bot": (BATTERY_DESCRIPTION,),
"Battery Circulator Fan": (BATTERY_DESCRIPTION,),
"Meter": (
TEMPERATURE_DESCRIPTION,
HUMIDITY_DESCRIPTION,

View File

@ -165,6 +165,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN][entry.entry_id] = YoLinkHomeStore(
yolink_home, device_coordinators
)
# Clean up yolink devices which are not associated to the account anymore.
device_registry = dr.async_get(hass)
device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
for device_entry in device_entries:
for identifier in device_entry.identifiers:
if (
identifier[0] == DOMAIN
and device_coordinators.get(identifier[1]) is None
):
device_registry.async_update_device(
device_entry.id, remove_config_entry_id=entry.entry_id
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def async_yolink_unload(event) -> None:

View File

@ -0,0 +1,187 @@
"""Test for the Switchbot Battery Circulator Fan."""
from unittest.mock import patch
from switchbot_api import Device, SwitchBotAPI
from homeassistant.components.fan import (
ATTR_PERCENTAGE,
ATTR_PRESET_MODE,
DOMAIN as FAN_DOMAIN,
SERVICE_SET_PERCENTAGE,
SERVICE_SET_PRESET_MODE,
SERVICE_TURN_ON,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant
from . import configure_integration
async def test_coordinator_data_is_none(
hass: HomeAssistant, mock_list_devices, mock_get_status
) -> None:
"""Test coordinator data is none."""
mock_list_devices.return_value = [
Device(
version="V1.0",
deviceId="battery-fan-id-1",
deviceName="battery-fan-1",
deviceType="Battery Circulator Fan",
hubDeviceId="test-hub-id",
),
]
mock_get_status.side_effect = [
None,
]
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
entity_id = "fan.battery_fan_1"
state = hass.states.get(entity_id)
assert state.state == STATE_UNKNOWN
async def test_turn_on(hass: HomeAssistant, mock_list_devices, mock_get_status) -> None:
"""Test turning on the fan."""
mock_list_devices.return_value = [
Device(
version="V1.0",
deviceId="battery-fan-id-1",
deviceName="battery-fan-1",
deviceType="Battery Circulator Fan",
hubDeviceId="test-hub-id",
),
]
mock_get_status.side_effect = [
{"power": "off", "mode": "direct", "fanSpeed": "0"},
{"power": "on", "mode": "direct", "fanSpeed": "0"},
{"power": "on", "mode": "direct", "fanSpeed": "0"},
]
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
entity_id = "fan.battery_fan_1"
state = hass.states.get(entity_id)
assert state.state == STATE_OFF
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
await hass.services.async_call(
FAN_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_send_command.assert_called()
state = hass.states.get(entity_id)
assert state.state == STATE_ON
async def test_turn_off(
hass: HomeAssistant, mock_list_devices, mock_get_status
) -> None:
"""Test turning off the fan."""
mock_list_devices.return_value = [
Device(
version="V1.0",
deviceId="battery-fan-id-1",
deviceName="battery-fan-1",
deviceType="Battery Circulator Fan",
hubDeviceId="test-hub-id",
),
]
mock_get_status.side_effect = [
{"power": "on", "mode": "direct", "fanSpeed": "0"},
{"power": "off", "mode": "direct", "fanSpeed": "0"},
{"power": "off", "mode": "direct", "fanSpeed": "0"},
]
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
entity_id = "fan.battery_fan_1"
state = hass.states.get(entity_id)
assert state.state == STATE_ON
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
await hass.services.async_call(
FAN_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_send_command.assert_called()
state = hass.states.get(entity_id)
assert state.state == STATE_OFF
async def test_set_percentage(
hass: HomeAssistant, mock_list_devices, mock_get_status
) -> None:
"""Test set percentage."""
mock_list_devices.return_value = [
Device(
version="V1.0",
deviceId="battery-fan-id-1",
deviceName="battery-fan-1",
deviceType="Battery Circulator Fan",
hubDeviceId="test-hub-id",
),
]
mock_get_status.side_effect = [
{"power": "on", "mode": "direct", "fanSpeed": "0"},
{"power": "on", "mode": "direct", "fanSpeed": "0"},
{"power": "off", "mode": "direct", "fanSpeed": "5"},
]
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
entity_id = "fan.battery_fan_1"
state = hass.states.get(entity_id)
assert state.state == STATE_ON
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
await hass.services.async_call(
FAN_DOMAIN,
SERVICE_SET_PERCENTAGE,
{ATTR_ENTITY_ID: entity_id, ATTR_PERCENTAGE: 5},
blocking=True,
)
mock_send_command.assert_called()
async def test_set_preset_mode(
hass: HomeAssistant, mock_list_devices, mock_get_status
) -> None:
"""Test set preset mode."""
mock_list_devices.return_value = [
Device(
version="V1.0",
deviceId="battery-fan-id-1",
deviceName="battery-fan-1",
deviceType="Battery Circulator Fan",
hubDeviceId="test-hub-id",
),
]
mock_get_status.side_effect = [
{"power": "on", "mode": "direct", "fanSpeed": "0"},
{"power": "on", "mode": "direct", "fanSpeed": "0"},
{"power": "on", "mode": "baby", "fanSpeed": "0"},
]
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
entity_id = "fan.battery_fan_1"
state = hass.states.get(entity_id)
assert state.state == STATE_ON
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
await hass.services.async_call(
FAN_DOMAIN,
SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "baby"},
blocking=True,
)
mock_send_command.assert_called_once()

View File

@ -75,6 +75,14 @@ DEVICE_MOCKS = {
Platform.BINARY_SENSOR,
Platform.SENSOR,
],
"qxj_temp_humidity_external_probe": [
# https://github.com/home-assistant/core/issues/136472
Platform.SENSOR,
],
"qxj_weather_station": [
# https://github.com/orgs/home-assistant/discussions/318
Platform.SENSOR,
],
"rqbj_gas_sensor": [
# https://github.com/orgs/home-assistant/discussions/100
Platform.BINARY_SENSOR,

View File

@ -0,0 +1,65 @@
{
"endpoint": "https://apigw.tuyaeu.com",
"terminal_id": "1708196692712PHOeqy",
"mqtt_connected": true,
"disabled_by": null,
"disabled_polling": false,
"id": "bff00f6abe0563b284t77p",
"name": "Frysen",
"category": "qxj",
"product_id": "is2indt9nlth6esa",
"product_name": "T & H Sensor with external probe",
"online": true,
"sub": false,
"time_zone": "+01:00",
"active_time": "2025-01-27T15:19:27+00:00",
"create_time": "2025-01-27T15:19:27+00:00",
"update_time": "2025-01-27T15:19:27+00:00",
"function": {},
"status_range": {
"temp_current": {
"type": "Integer",
"value": {
"unit": "\u2103",
"min": -99,
"max": 600,
"scale": 1,
"step": 1
}
},
"humidity_value": {
"type": "Integer",
"value": {
"unit": "%",
"min": 0,
"max": 100,
"scale": 0,
"step": 1
}
},
"battery_state": {
"type": "Enum",
"value": {
"range": ["low", "middle", "high"]
}
},
"temp_current_external": {
"type": "Integer",
"value": {
"unit": "\u2103",
"min": -400,
"max": 1200,
"scale": 1,
"step": 1
}
}
},
"status": {
"temp_current": 222,
"humidity_value": 38,
"battery_state": "high",
"temp_current_external": -130
},
"set_up": false,
"support_local": true
}

View File

@ -0,0 +1,412 @@
{
"endpoint": "https://apigw.tuyaeu.com",
"terminal_id": "1751921699759JsVujI",
"mqtt_connected": true,
"disabled_by": null,
"disabled_polling": false,
"id": "bf84c743a84eb2c8abeurz",
"name": "BR 7-in-1 WLAN Wetterstation Anthrazit",
"category": "qxj",
"product_id": "fsea1lat3vuktbt6",
"product_name": "BR 7-in-1 WLAN Wetterstation Anthrazit",
"online": true,
"sub": false,
"time_zone": "+02:00",
"active_time": "2025-07-07T17:43:41+00:00",
"create_time": "2025-07-07T17:43:41+00:00",
"update_time": "2025-07-07T17:43:41+00:00",
"function": {
"temp_unit_convert": {
"type": "Enum",
"value": {
"range": ["c", "f"]
}
},
"windspeed_unit_convert": {
"type": "Enum",
"value": {
"range": ["mph"]
}
},
"pressure_unit_convert": {
"type": "Enum",
"value": {
"range": ["hpa", "inhg", "mmhg"]
}
},
"rain_unit_convert": {
"type": "Enum",
"value": {
"range": ["mm", "inch"]
}
},
"bright_unit_convert": {
"type": "Enum",
"value": {
"range": ["lux", "fc", "wm2"]
}
}
},
"status_range": {
"temp_current": {
"type": "Integer",
"value": {
"unit": "\u2103",
"min": -400,
"max": 600,
"scale": 1,
"step": 1
}
},
"humidity_value": {
"type": "Integer",
"value": {
"unit": "%",
"min": 0,
"max": 100,
"scale": 0,
"step": 1
}
},
"battery_state": {
"type": "Enum",
"value": {
"range": ["low", "high"]
}
},
"temp_unit_convert": {
"type": "Enum",
"value": {
"range": ["c", "f"]
}
},
"windspeed_unit_convert": {
"type": "Enum",
"value": {
"range": ["mph"]
}
},
"pressure_unit_convert": {
"type": "Enum",
"value": {
"range": ["hpa", "inhg", "mmhg"]
}
},
"rain_unit_convert": {
"type": "Enum",
"value": {
"range": ["mm", "inch"]
}
},
"bright_unit_convert": {
"type": "Enum",
"value": {
"range": ["lux", "fc", "wm2"]
}
},
"fault_type": {
"type": "Enum",
"value": {
"range": [
"normal",
"ch1_offline",
"ch2_offline",
"ch3_offline",
"offline"
]
}
},
"battery_status": {
"type": "Enum",
"value": {
"range": ["low", "high"]
}
},
"battery_state_1": {
"type": "Enum",
"value": {
"range": ["low", "high"]
}
},
"battery_state_2": {
"type": "Enum",
"value": {
"range": ["low", "high"]
}
},
"battery_state_3": {
"type": "Enum",
"value": {
"range": ["low", "high"]
}
},
"temp_current_external": {
"type": "Integer",
"value": {
"unit": "\u2103",
"min": -400,
"max": 600,
"scale": 1,
"step": 1
}
},
"humidity_outdoor": {
"type": "Integer",
"value": {
"unit": "%",
"min": 0,
"max": 100,
"scale": 0,
"step": 1
}
},
"temp_current_external_1": {
"type": "Integer",
"value": {
"unit": "\u2103",
"min": -400,
"max": 600,
"scale": 1,
"step": 1
}
},
"humidity_outdoor_1": {
"type": "Integer",
"value": {
"unit": "%",
"min": 0,
"max": 100,
"scale": 0,
"step": 1
}
},
"temp_current_external_2": {
"type": "Integer",
"value": {
"unit": "\u2103",
"min": -400,
"max": 600,
"scale": 1,
"step": 1
}
},
"humidity_outdoor_2": {
"type": "Integer",
"value": {
"unit": "%",
"min": 0,
"max": 100,
"scale": 0,
"step": 1
}
},
"temp_current_external_3": {
"type": "Integer",
"value": {
"unit": "\u2103",
"min": -400,
"max": 600,
"scale": 1,
"step": 1
}
},
"humidity_outdoor_3": {
"type": "Integer",
"value": {
"unit": "%",
"min": 0,
"max": 100,
"scale": 0,
"step": 1
}
},
"atmospheric_pressture": {
"type": "Integer",
"value": {
"unit": "hPa",
"min": 3000,
"max": 12000,
"scale": 1,
"step": 1
}
},
"pressure_drop": {
"type": "Integer",
"value": {
"unit": "hPa",
"min": 0,
"max": 15,
"scale": 0,
"step": 1
}
},
"windspeed_avg": {
"type": "Integer",
"value": {
"unit": "m/s",
"min": 0,
"max": 700,
"scale": 1,
"step": 1
}
},
"windspeed_gust": {
"type": "Integer",
"value": {
"unit": "m/s",
"min": 0,
"max": 700,
"scale": 1,
"step": 1
}
},
"wind_direct": {
"type": "Enum",
"value": {
"range": [
"north",
"north_north_east",
"north_east",
"east_north_east",
"east",
"east_south_east",
"south_east",
"south_south_east",
"south",
"south_south_west",
"south_west",
"west_south_west",
"west",
"west_north_west",
"north_west",
"north_north_west"
]
}
},
"rain_24h": {
"type": "Integer",
"value": {
"unit": "mm",
"min": 0,
"max": 1000000,
"scale": 3,
"step": 1
}
},
"rain_rate": {
"type": "Integer",
"value": {
"unit": "mm",
"min": 0,
"max": 999999,
"scale": 3,
"step": 1
}
},
"uv_index": {
"type": "Integer",
"value": {
"unit": "",
"min": 0,
"max": 180,
"scale": 1,
"step": 1
}
},
"bright_value": {
"type": "Integer",
"value": {
"unit": "lux",
"min": 0,
"max": 238000,
"scale": 0,
"step": 100
}
},
"dew_point_temp": {
"type": "Integer",
"value": {
"unit": "\u2103",
"min": -400,
"max": 800,
"scale": 1,
"step": 1
}
},
"feellike_temp": {
"type": "Integer",
"value": {
"unit": "\u2103",
"min": -650,
"max": 500,
"scale": 1,
"step": 1
}
},
"heat_index": {
"type": "Integer",
"value": {
"unit": "\u2103",
"min": 260,
"max": 500,
"scale": 1,
"step": 1
}
},
"windchill_index": {
"type": "Integer",
"value": {
"unit": "\u2103",
"min": -650,
"max": 600,
"scale": 1,
"step": 1
}
},
"com_index": {
"type": "Enum",
"value": {
"range": ["moist", "dry", "comfortable"]
}
}
},
"status": {
"temp_current": 240,
"humidity_value": 52,
"battery_state": "high",
"temp_unit_convert": "c",
"windspeed_unit_convert": "m_s",
"pressure_unit_convert": "hpa",
"rain_unit_convert": "mm",
"bright_unit_convert": "lux",
"fault_type": "normal",
"battery_status": "low",
"battery_state_1": "high",
"battery_state_2": "high",
"battery_state_3": "low",
"temp_current_external": -400,
"humidity_outdoor": 0,
"temp_current_external_1": 193,
"humidity_outdoor_1": 99,
"temp_current_external_2": 252,
"humidity_outdoor_2": 0,
"temp_current_external_3": -400,
"humidity_outdoor_3": 0,
"atmospheric_pressture": 10040,
"pressure_drop": 0,
"windspeed_avg": 0,
"windspeed_gust": 0,
"wind_direct": "none",
"rain_24h": 0,
"rain_rate": 0,
"uv_index": 0,
"bright_value": 0,
"dew_point_temp": -400,
"feellike_temp": -650,
"heat_index": 260,
"windchill_index": -650,
"com_index": "none"
},
"set_up": false,
"support_local": true
}

View File

@ -1265,6 +1265,485 @@
'state': '100.0',
})
# ---
# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_battery_state-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.frysen_battery_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Battery state',
'platform': 'tuya',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'battery_state',
'unique_id': 'tuya.bff00f6abe0563b284t77pbattery_state',
'unit_of_measurement': None,
})
# ---
# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_battery_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Frysen Battery state',
}),
'context': <ANY>,
'entity_id': 'sensor.frysen_battery_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'high',
})
# ---
# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_humidity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.frysen_humidity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
'original_icon': None,
'original_name': 'Humidity',
'platform': 'tuya',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'humidity',
'unique_id': 'tuya.bff00f6abe0563b284t77phumidity_value',
'unit_of_measurement': '%',
})
# ---
# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_humidity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'humidity',
'friendly_name': 'Frysen Humidity',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.frysen_humidity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '38.0',
})
# ---
# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_probe_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.frysen_probe_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Probe temperature',
'platform': 'tuya',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature_external',
'unique_id': 'tuya.bff00f6abe0563b284t77ptemp_current_external',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_probe_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Frysen Probe temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.frysen_probe_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '-13.0',
})
# ---
# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.frysen_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Temperature',
'platform': 'tuya',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature',
'unique_id': 'tuya.bff00f6abe0563b284t77ptemp_current',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_platform_setup_and_discovery[qxj_temp_humidity_external_probe][sensor.frysen_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Frysen Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.frysen_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '22.2',
})
# ---
# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Battery state',
'platform': 'tuya',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'battery_state',
'unique_id': 'tuya.bf84c743a84eb2c8abeurzbattery_state',
'unit_of_measurement': None,
})
# ---
# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Battery state',
}),
'context': <ANY>,
'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_battery_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'high',
})
# ---
# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
'original_icon': None,
'original_name': 'Humidity',
'platform': 'tuya',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'humidity',
'unique_id': 'tuya.bf84c743a84eb2c8abeurzhumidity_value',
'unit_of_measurement': '%',
})
# ---
# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'humidity',
'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Humidity',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_humidity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '52.0',
})
# ---
# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ILLUMINANCE: 'illuminance'>,
'original_icon': None,
'original_name': 'Illuminance',
'platform': 'tuya',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'illuminance',
'unique_id': 'tuya.bf84c743a84eb2c8abeurzbright_value',
'unit_of_measurement': 'lx',
})
# ---
# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'illuminance',
'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Illuminance',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'lx',
}),
'context': <ANY>,
'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_illuminance',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.0',
})
# ---
# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Probe temperature',
'platform': 'tuya',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature_external',
'unique_id': 'tuya.bf84c743a84eb2c8abeurztemp_current_external',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Probe temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_probe_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '-40.0',
})
# ---
# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Temperature',
'platform': 'tuya',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'temperature',
'unique_id': 'tuya.bf84c743a84eb2c8abeurztemp_current',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_platform_setup_and_discovery[qxj_weather_station][sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'BR 7-in-1 WLAN Wetterstation Anthrazit Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.br_7_in_1_wlan_wetterstation_anthrazit_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '24.0',
})
# ---
# name: test_platform_setup_and_discovery[rqbj_gas_sensor][sensor.gas_sensor_gas-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -0,0 +1,77 @@
"""Provide common fixtures for the YoLink integration tests."""
from __future__ import annotations
from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from yolink.home_manager import YoLinkHome
from homeassistant.components.application_credentials import (
ClientCredential,
async_import_client_credential,
)
from homeassistant.components.yolink.api import ConfigEntryAuth
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
CLIENT_ID = "12345"
CLIENT_SECRET = "6789"
DOMAIN = "yolink"
@pytest.fixture
async def setup_credentials(hass: HomeAssistant) -> None:
"""Fixture to setup credentials."""
assert await async_setup_component(hass, "application_credentials", {})
await async_import_client_credential(
hass,
DOMAIN,
ClientCredential(CLIENT_ID, CLIENT_SECRET),
)
@pytest.fixture(name="mock_auth_manager")
def mock_auth_manager() -> Generator[MagicMock]:
"""Mock the authentication manager."""
with patch(
"homeassistant.components.yolink.api.ConfigEntryAuth", autospec=True
) as mock_auth:
mock_auth.return_value = MagicMock(spec=ConfigEntryAuth)
yield mock_auth
@pytest.fixture(name="mock_yolink_home")
def mock_yolink_home() -> Generator[AsyncMock]:
"""Mock YoLink home instance."""
with patch(
"homeassistant.components.yolink.YoLinkHome", autospec=True
) as mock_home:
mock_home.return_value = AsyncMock(spec=YoLinkHome)
yield mock_home
@pytest.fixture
def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
"""Mock a config entry for YoLink."""
config_entry = MockConfigEntry(
unique_id=DOMAIN,
domain=DOMAIN,
title="yolink",
data={
"auth_implementation": DOMAIN,
"token": {
"refresh_token": "mock-refresh-token",
"access_token": "mock-access-token",
"type": "Bearer",
"expires_in": 60,
"scope": "create",
},
},
options={},
)
config_entry.add_to_hass(hass)
return config_entry

View File

@ -0,0 +1,38 @@
"""Tests for the yolink integration."""
import pytest
from homeassistant.components.yolink import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("setup_credentials", "mock_auth_manager", "mock_yolink_home")
async def test_device_remove_devices(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test we can only remove a device that no longer exists."""
device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={(DOMAIN, "stale_device_id")},
)
device_entries = dr.async_entries_for_config_entry(
device_registry, mock_config_entry.entry_id
)
assert len(device_entries) == 1
device_entry = device_entries[0]
assert device_entry.identifiers == {(DOMAIN, "stale_device_id")}
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
device_entries = dr.async_entries_for_config_entry(
device_registry, mock_config_entry.entry_id
)
assert len(device_entries) == 0