mirror of
https://github.com/home-assistant/core.git
synced 2026-05-05 12:24:48 +02:00
Add garage door opener for switchbot integration (#148460)
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user