Add garage door opener for switchbot integration (#148460)

This commit is contained in:
Retha Runolfsson
2025-09-26 18:11:59 +08:00
committed by GitHub
parent ec0380fd3b
commit 7a4d75bc44
6 changed files with 169 additions and 16 deletions
@@ -100,6 +100,7 @@ PLATFORMS_BY_TYPE = {
SupportedModels.RGBICWW_STRIP_LIGHT.value: [Platform.LIGHT, Platform.SENSOR],
SupportedModels.PLUG_MINI_EU.value: [Platform.SWITCH, Platform.SENSOR],
SupportedModels.RELAY_SWITCH_2PM.value: [Platform.SWITCH, Platform.SENSOR],
SupportedModels.GARAGE_DOOR_OPENER.value: [Platform.COVER, Platform.SENSOR],
}
CLASS_BY_DEVICE = {
SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight,
@@ -133,6 +134,7 @@ CLASS_BY_DEVICE = {
SupportedModels.RGBICWW_STRIP_LIGHT.value: switchbot.SwitchbotRgbicLight,
SupportedModels.PLUG_MINI_EU.value: switchbot.SwitchbotRelaySwitch,
SupportedModels.RELAY_SWITCH_2PM.value: switchbot.SwitchbotRelaySwitch2PM,
SupportedModels.GARAGE_DOOR_OPENER.value: switchbot.SwitchbotGarageDoorOpener,
}
@@ -56,6 +56,7 @@ class SupportedModels(StrEnum):
PLUG_MINI_EU = "plug_mini_eu"
RELAY_SWITCH_2PM = "relay_switch_2pm"
K11_PLUS_VACUUM = "k11+_vacuum"
GARAGE_DOOR_OPENER = "garage_door_opener"
CONNECTABLE_SUPPORTED_MODEL_TYPES = {
@@ -91,6 +92,7 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = {
SwitchbotModel.PLUG_MINI_EU: SupportedModels.PLUG_MINI_EU,
SwitchbotModel.RELAY_SWITCH_2PM: SupportedModels.RELAY_SWITCH_2PM,
SwitchbotModel.K11_VACUUM: SupportedModels.K11_PLUS_VACUUM,
SwitchbotModel.GARAGE_DOOR_OPENER: SupportedModels.GARAGE_DOOR_OPENER,
}
NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = {
@@ -126,6 +128,7 @@ ENCRYPTED_MODELS = {
SwitchbotModel.RGBICWW_FLOOR_LAMP,
SwitchbotModel.PLUG_MINI_EU,
SwitchbotModel.RELAY_SWITCH_2PM,
SwitchbotModel.GARAGE_DOOR_OPENER,
}
ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[
@@ -146,6 +149,7 @@ ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[
SwitchbotModel.RGBICWW_FLOOR_LAMP: switchbot.SwitchbotRgbicLight,
SwitchbotModel.PLUG_MINI_EU: switchbot.SwitchbotRelaySwitch,
SwitchbotModel.RELAY_SWITCH_2PM: switchbot.SwitchbotRelaySwitch2PM,
SwitchbotModel.GARAGE_DOOR_OPENER: switchbot.SwitchbotRelaySwitch,
}
HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL = {
+30 -1
View File
@@ -35,7 +35,9 @@ async def async_setup_entry(
) -> None:
"""Set up Switchbot curtain based on a config entry."""
coordinator = entry.runtime_data
if isinstance(coordinator.device, switchbot.SwitchbotBlindTilt):
if isinstance(coordinator.device, switchbot.SwitchbotGarageDoorOpener):
async_add_entities([SwitchbotGarageDoorOpenerEntity(coordinator)])
elif isinstance(coordinator.device, switchbot.SwitchbotBlindTilt):
async_add_entities([SwitchBotBlindTiltEntity(coordinator)])
elif isinstance(coordinator.device, switchbot.SwitchbotRollerShade):
async_add_entities([SwitchBotRollerShadeEntity(coordinator)])
@@ -295,3 +297,30 @@ class SwitchBotRollerShadeEntity(SwitchbotEntity, CoverEntity, RestoreEntity):
self._attr_is_closed = self.parsed_data["position"] <= 20
self.async_write_ha_state()
class SwitchbotGarageDoorOpenerEntity(SwitchbotEntity, CoverEntity):
"""Representation of a Switchbot garage door."""
_device: switchbot.SwitchbotGarageDoorOpener
_attr_device_class = CoverDeviceClass.GARAGE
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
_attr_translation_key = "garage_door"
_attr_name = None
@property
def is_closed(self) -> bool | None:
"""Return true if cover is closed, else False."""
return not self._device.door_open()
@exception_handler
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the garage door."""
await self._device.open()
self.async_write_ha_state()
@exception_handler
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the garage door."""
await self._device.close()
self.async_write_ha_state()
+44
View File
@@ -1127,3 +1127,47 @@ K11_PLUS_VACUUM_SERVICE_INFO = BluetoothServiceInfoBleak(
connectable=True,
tx_power=-127,
)
RELAY_SWITCH_1_SERVICE_INFO = BluetoothServiceInfoBleak(
name="Relay Switch 1",
manufacturer_data={2409: b"$X|\x0866G\x81\x00\x00\x001\x00\x00\x00\x00"},
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b";\x00\x00\x00"},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
address="AA:BB:CC:DD:EE:FF",
rssi=-60,
source="local",
advertisement=generate_advertisement_data(
local_name="Relay Switch 1",
manufacturer_data={2409: b"$X|\x0866G\x81\x00\x00\x001\x00\x00\x00\x00"},
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"=\x00\x00\x00"},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
),
device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Relay Switch 1"),
time=0,
connectable=True,
tx_power=-127,
)
GARAGE_DOOR_OPENER_SERVICE_INFO = BluetoothServiceInfoBleak(
name="Garage Door Opener",
manufacturer_data={2409: b"$X|\x05BN\x0f\x00\x00\x03\x00\x00\x00\x00\x00\x00"},
service_data={
"0000fd3d-0000-1000-8000-00805f9b34fb": b">\x00\x00\x00",
},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
address="AA:BB:CC:DD:EE:FF",
rssi=-60,
source="local",
advertisement=generate_advertisement_data(
local_name="Garage Door Opener",
manufacturer_data={2409: b"$X|\x05BN\x0f\x00\x00\x03\x00\x00\x00\x00\x00\x00"},
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b">\x00\x00\x00"},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
),
device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Garage Door Opener"),
time=0,
connectable=True,
tx_power=-127,
)
+39
View File
@@ -30,6 +30,7 @@ from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
from . import (
GARAGE_DOOR_OPENER_SERVICE_INFO,
ROLLER_SHADE_SERVICE_INFO,
WOBLINDTILT_SERVICE_INFO,
WOCURTAIN3_SERVICE_INFO,
@@ -648,3 +649,41 @@ async def test_exception_handling_cover_service(
{**service_data, ATTR_ENTITY_ID: entity_id},
blocking=True,
)
@pytest.mark.parametrize(
("service", "mock_method"),
[
(SERVICE_OPEN_COVER, "open"),
(SERVICE_CLOSE_COVER, "close"),
],
)
async def test_garage_door_opener_controlling(
hass: HomeAssistant,
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
service: str,
mock_method: str,
) -> None:
"""Test Garage Door Opener controlling."""
inject_bluetooth_service_info(hass, GARAGE_DOOR_OPENER_SERVICE_INFO)
entry = mock_entry_encrypted_factory(sensor_type="garage_door_opener")
entry.add_to_hass(hass)
entity_id = "cover.test_name"
mocked_instance = AsyncMock(return_value=True)
with patch.multiple(
"homeassistant.components.switchbot.cover.switchbot.SwitchbotGarageDoorOpener",
update=AsyncMock(),
**{mock_method: mocked_instance},
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
COVER_DOMAIN,
service,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
mocked_instance.assert_awaited_once()
+50 -15
View File
@@ -19,8 +19,10 @@ from homeassistant.exceptions import HomeAssistantError
from . import (
PLUG_MINI_EU_SERVICE_INFO,
RELAY_SWITCH_1_SERVICE_INFO,
RELAY_SWITCH_2PM_SERVICE_INFO,
WOHAND_SERVICE_INFO,
WORELAY_SWITCH_1PM_SERVICE_INFO,
)
from tests.common import MockConfigEntry, mock_restore_cache
@@ -114,6 +116,8 @@ async def test_exception_handling_switch(
("sensor_type", "service_info"),
[
("plug_mini_eu", PLUG_MINI_EU_SERVICE_INFO),
("relay_switch_1", RELAY_SWITCH_1_SERVICE_INFO),
("relay_switch_1pm", WORELAY_SWITCH_1PM_SERVICE_INFO),
],
)
@pytest.mark.parametrize(
@@ -207,11 +211,37 @@ async def test_relay_switch_2pm_control(
@pytest.mark.parametrize(
("exception", "error_message"),
("sensor_type", "service_info", "entity_id", "mock_class"),
[
(
SwitchbotOperationError("Operation failed"),
"An error occurred while performing the action: Operation failed",
"relay_switch_1",
RELAY_SWITCH_1_SERVICE_INFO,
"switch.test_name",
"SwitchbotRelaySwitch",
),
(
"relay_switch_1pm",
WORELAY_SWITCH_1PM_SERVICE_INFO,
"switch.test_name",
"SwitchbotRelaySwitch",
),
(
"plug_mini_eu",
PLUG_MINI_EU_SERVICE_INFO,
"switch.test_name",
"SwitchbotRelaySwitch",
),
(
"relay_switch_2pm",
RELAY_SWITCH_2PM_SERVICE_INFO,
"switch.test_name_channel_1",
"SwitchbotRelaySwitch2PM",
),
(
"relay_switch_2pm",
RELAY_SWITCH_2PM_SERVICE_INFO,
"switch.test_name_channel_2",
"SwitchbotRelaySwitch2PM",
),
],
)
@@ -223,29 +253,34 @@ async def test_relay_switch_2pm_control(
],
)
@pytest.mark.parametrize(
"entry_id",
("exception", "error_message"),
[
"switch.test_name_channel_1",
"switch.test_name_channel_2",
(
SwitchbotOperationError("Operation failed"),
"An error occurred while performing the action: Operation failed",
),
],
)
async def test_relay_switch_2pm_exception(
async def test_relay_switch_control_with_exception(
hass: HomeAssistant,
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
exception: Exception,
error_message: str,
sensor_type: str,
service_info: BluetoothServiceInfoBleak,
entity_id: str,
mock_class: str,
service: str,
mock_method: str,
entry_id: str,
exception: Exception,
error_message: str,
) -> None:
"""Test Relay Switch 2PM exception handling."""
inject_bluetooth_service_info(hass, RELAY_SWITCH_2PM_SERVICE_INFO)
"""Test Relay Switch control with exception."""
inject_bluetooth_service_info(hass, service_info)
entry = mock_entry_encrypted_factory(sensor_type="relay_switch_2pm")
entry = mock_entry_encrypted_factory(sensor_type=sensor_type)
entry.add_to_hass(hass)
with patch.multiple(
"homeassistant.components.switchbot.switch.switchbot.SwitchbotRelaySwitch2PM",
f"homeassistant.components.switchbot.switch.switchbot.{mock_class}",
update=AsyncMock(return_value=None),
**{mock_method: AsyncMock(side_effect=exception)},
):
@@ -256,6 +291,6 @@ async def test_relay_switch_2pm_exception(
await hass.services.async_call(
SWITCH_DOMAIN,
service,
{ATTR_ENTITY_ID: entry_id},
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)