Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add switchbot cloud integration #99607

Merged
merged 72 commits into from
Sep 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
3db8e25
Switches via API
SeraphicRav Sep 3, 2023
22ddc4f
Using external library
SeraphicRav Sep 3, 2023
7f99cea
UT and checlist
SeraphicRav Sep 4, 2023
306d1d5
Updating file .coveragerc
SeraphicRav Sep 4, 2023
fdc6e3e
Merge branch 'dev' into hacking-around
SeraphicRav Sep 4, 2023
aebaac8
Merge branch 'dev' into hacking-around
SeraphicRav Sep 4, 2023
64d36dc
Update homeassistant/components/switchbot_via_api/switch.py
SeraphicRav Sep 5, 2023
88a361e
Update homeassistant/components/switchbot_via_api/switch.py
SeraphicRav Sep 5, 2023
49d8dc3
Update homeassistant/components/switchbot_via_api/switch.py
SeraphicRav Sep 5, 2023
b8965e6
Review fixes
SeraphicRav Sep 5, 2023
68e173f
Merge branch 'dev' into hacking-around
SeraphicRav Sep 5, 2023
9a989e3
Merge branch 'dev' into hacking-around
SeraphicRav Sep 6, 2023
41fa12c
Merge branch 'dev' into hacking-around
SeraphicRav Sep 6, 2023
28998d7
Merge branch 'dev' into hacking-around
SeraphicRav Sep 6, 2023
58fe468
Merge branch 'dev' into hacking-around
SeraphicRav Sep 6, 2023
fed3142
Merge branch 'dev' into hacking-around
SeraphicRav Sep 6, 2023
81f1cf5
Merge branch 'dev' into hacking-around
SeraphicRav Sep 6, 2023
51b6a00
Merge branch 'dev' into hacking-around
SeraphicRav Sep 6, 2023
7a8e9d6
Merge branch 'dev' into hacking-around
SeraphicRav Sep 6, 2023
ccab66b
Merge branch 'dev' into hacking-around
SeraphicRav Sep 7, 2023
4f70824
Apply suggestions from code review
SeraphicRav Sep 7, 2023
5b1f34d
Merge branch 'dev' into hacking-around
SeraphicRav Sep 7, 2023
cc0da4e
Merge branch 'dev' into hacking-around
SeraphicRav Sep 7, 2023
c2b7528
This base class shouldn't know about Remote
SeraphicRav Sep 7, 2023
9afcb84
Fixing suggestion
SeraphicRav Sep 7, 2023
f3f8684
Sometimes, the state from the API is not updated immediately
SeraphicRav Sep 7, 2023
c8cdcb0
Merge branch 'dev' into hacking-around
SeraphicRav Sep 7, 2023
8f498cc
Merge branch 'dev' into hacking-around
SeraphicRav Sep 7, 2023
3be5dae
Merge branch 'dev' into hacking-around
SeraphicRav Sep 7, 2023
ea9eb36
Merge branch 'dev' into hacking-around
SeraphicRav Sep 7, 2023
410014b
Merge branch 'dev' into hacking-around
SeraphicRav Sep 10, 2023
3dc912d
Review changes
SeraphicRav Sep 10, 2023
9800e6d
Merge branch 'dev' into hacking-around
SeraphicRav Sep 10, 2023
17761b0
Merge branch 'dev' into hacking-around
SeraphicRav Sep 10, 2023
ef22962
Some review changes
SeraphicRav Sep 10, 2023
bd845e2
Merge branch 'hacking-around' of https://github.com/SeraphicCorp/home…
SeraphicRav Sep 10, 2023
5862ec7
Merge branch 'dev' into hacking-around
SeraphicRav Sep 10, 2023
4949070
Review changes
SeraphicRav Sep 10, 2023
fda34a4
Merge branch 'hacking-around' of https://github.com/SeraphicCorp/home…
SeraphicRav Sep 10, 2023
613b718
Review change: Adding type on commands
SeraphicRav Sep 10, 2023
b25c98f
Parameterizing some tests
SeraphicRav Sep 10, 2023
155e538
Merge branch 'dev' into hacking-around
SeraphicRav Sep 10, 2023
e48324a
Merge branch 'dev' into hacking-around
SeraphicRav Sep 10, 2023
51dedb0
Merge branch 'dev' into hacking-around
SeraphicRav Sep 10, 2023
86620d2
Review changes
SeraphicRav Sep 10, 2023
e8a00f0
Merge branch 'dev' into hacking-around
SeraphicRav Sep 10, 2023
2abe065
Updating .coveragerc
SeraphicRav Sep 10, 2023
1dd1f32
Merge branch 'hacking-around' of https://github.com/SeraphicCorp/home…
SeraphicRav Sep 10, 2023
67b9f35
Fixing error handling in coordinator
SeraphicRav Sep 10, 2023
33bbe05
Merge branch 'dev' into hacking-around
SeraphicRav Sep 10, 2023
50da148
Review changes
SeraphicRav Sep 10, 2023
379ba79
Merge branch 'dev' into hacking-around
SeraphicRav Sep 10, 2023
4bdc639
Review changes
SeraphicRav Sep 10, 2023
bb30477
Merge branch 'dev' into hacking-around
SeraphicRav Sep 10, 2023
4641ff7
Merge branch 'dev' into hacking-around
SeraphicRav Sep 11, 2023
bf2fa52
Adding switchbot brand
SeraphicRav Sep 11, 2023
2f3a29d
Merge branch 'hacking-around' of https://github.com/SeraphicCorp/home…
SeraphicRav Sep 11, 2023
85908eb
Merge branch 'dev' into hacking-around
SeraphicRav Sep 11, 2023
a20b64c
Merge branch 'dev' into hacking-around
SeraphicRav Sep 11, 2023
c5afc68
Merge branch 'dev' into hacking-around
SeraphicRav Sep 11, 2023
b47fca3
Merge branch 'dev' into hacking-around
SeraphicRav Sep 11, 2023
339ad37
Merge branch 'dev' into hacking-around
SeraphicRav Sep 12, 2023
9a07b54
Apply suggestions from code review
SeraphicRav Sep 12, 2023
a2d4bae
Review changes
SeraphicRav Sep 12, 2023
714246b
Adding strict typing
SeraphicRav Sep 12, 2023
79948c0
Merge branch 'dev' into hacking-around
SeraphicRav Sep 12, 2023
16f2856
Merge branch 'dev' into hacking-around
SeraphicRav Sep 13, 2023
c6d3ffc
Merge branch 'dev' into hacking-around
SeraphicRav Sep 13, 2023
409a27a
Merge branch 'dev' into hacking-around
SeraphicRav Sep 14, 2023
a0f966f
Removing log in constructor
SeraphicRav Sep 14, 2023
259369d
Merge branch 'hacking-around' of https://github.com/SeraphicCorp/home…
SeraphicRav Sep 14, 2023
097abb1
Merge branch 'dev' into hacking-around
SeraphicRav Sep 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,9 @@ omit =
homeassistant/components/switchbot/sensor.py
homeassistant/components/switchbot/switch.py
homeassistant/components/switchbot/lock.py
homeassistant/components/switchbot_cloud/coordinator.py
homeassistant/components/switchbot_cloud/entity.py
homeassistant/components/switchbot_cloud/switch.py
homeassistant/components/switchmate/switch.py
homeassistant/components/syncthing/__init__.py
homeassistant/components/syncthing/sensor.py
Expand Down
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ homeassistant.components.sun.*
homeassistant.components.surepetcare.*
homeassistant.components.switch.*
homeassistant.components.switchbee.*
homeassistant.components.switchbot_cloud.*
homeassistant.components.switcher_kis.*
homeassistant.components.synology_dsm.*
homeassistant.components.systemmonitor.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,8 @@ build.json @home-assistant/supervisor
/tests/components/switchbee/ @jafar-atili
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/homeassistant/components/switchbot_cloud/ @SeraphicRav
/tests/components/switchbot_cloud/ @SeraphicRav
/homeassistant/components/switcher_kis/ @thecode
/tests/components/switcher_kis/ @thecode
/homeassistant/components/switchmate/ @danielhiversen @qiz-li
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/brands/switchbot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"domain": "switchbot",
"name": "SwitchBot",
"integrations": ["switchbot", "switchbot_cloud"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/switchbot/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"domain": "switchbot",
"name": "SwitchBot",
"name": "SwitchBot Bluetooth",
"bluetooth": [
{
"service_data_uuid": "00000d00-0000-1000-8000-00805f9b34fb",
Expand Down
81 changes: 81 additions & 0 deletions homeassistant/components/switchbot_cloud/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""The SwitchBot via API integration."""
from asyncio import gather
from dataclasses import dataclass
from logging import getLogger

from switchbot_api import CannotConnect, Device, InvalidAuth, Remote, SwitchBotAPI

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady

from .const import DOMAIN
from .coordinator import SwitchBotCoordinator

_LOGGER = getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SWITCH]


@dataclass
class SwitchbotDevices:
"""Switchbot devices data."""

switches: list[Device | Remote]


@dataclass
class SwitchbotCloudData:
"""Data to use in platforms."""

api: SwitchBotAPI
devices: SwitchbotDevices


async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool:
"""Set up SwitchBot via API from a config entry."""
token = config.data[CONF_API_TOKEN]
secret = config.data[CONF_API_KEY]

api = SwitchBotAPI(token=token, secret=secret)
try:
devices = await api.list_devices()
except InvalidAuth as ex:
_LOGGER.error(
"Invalid authentication while connecting to SwitchBot API: %s", ex
)
return False
except CannotConnect as ex:
raise ConfigEntryNotReady from ex
SeraphicRav marked this conversation as resolved.
Show resolved Hide resolved
_LOGGER.debug("Devices: %s", devices)
devices_and_coordinators = [
(device, SwitchBotCoordinator(hass, api, device)) for device in devices
]
bdraco marked this conversation as resolved.
Show resolved Hide resolved
hass.data.setdefault(DOMAIN, {})
data = SwitchbotCloudData(
api=api,
devices=SwitchbotDevices(
switches=[
(device, coordinator)
for device, coordinator in devices_and_coordinators
if isinstance(device, Device)
and device.device_type.startswith("Plug")
or isinstance(device, Remote)
],
),
)
hass.data[DOMAIN][config.entry_id] = data
_LOGGER.debug("Switches: %s", data.devices.switches)
await hass.config_entries.async_forward_entry_setups(config, PLATFORMS)
await gather(
*[coordinator.async_refresh() for _, coordinator in devices_and_coordinators]
bdraco marked this conversation as resolved.
Show resolved Hide resolved
)
SeraphicRav marked this conversation as resolved.
Show resolved Hide resolved
return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok
56 changes: 56 additions & 0 deletions homeassistant/components/switchbot_cloud/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Config flow for SwitchBot via API integration."""

from logging import getLogger
from typing import Any

from switchbot_api import CannotConnect, InvalidAuth, SwitchBotAPI
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN
from homeassistant.data_entry_flow import FlowResult

from .const import DOMAIN, ENTRY_TITLE

_LOGGER = getLogger(__name__)

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_API_TOKEN): str,
vol.Required(CONF_API_KEY): str,
}
)


class SwitchBotCloudConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for SwitchBot via API."""

VERSION = 1

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
try:
await SwitchBotAPI(
token=user_input[CONF_API_TOKEN], secret=user_input[CONF_API_KEY]
).list_devices()
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
SeraphicRav marked this conversation as resolved.
Show resolved Hide resolved
errors["base"] = "unknown"
else:
await self.async_set_unique_id(
user_input[CONF_API_TOKEN], raise_on_progress=False
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=ENTRY_TITLE, data=user_input)

return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
7 changes: 7 additions & 0 deletions homeassistant/components/switchbot_cloud/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Constants for the SwitchBot Cloud integration."""
from datetime import timedelta
from typing import Final

DOMAIN: Final = "switchbot_cloud"
ENTRY_TITLE = "SwitchBot Cloud"
SCAN_INTERVAL = timedelta(seconds=600)
50 changes: 50 additions & 0 deletions homeassistant/components/switchbot_cloud/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""SwitchBot Cloud coordinator."""
from asyncio import timeout
from logging import getLogger
from typing import Any

from switchbot_api import CannotConnect, Device, Remote, SwitchBotAPI

from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN, SCAN_INTERVAL

_LOGGER = getLogger(__name__)

Status = dict[str, Any] | None


class SwitchBotCoordinator(DataUpdateCoordinator[Status]):
"""SwitchBot Cloud coordinator."""

_api: SwitchBotAPI
_device_id: str
_should_poll = False

def __init__(
self, hass: HomeAssistant, api: SwitchBotAPI, device: Device | Remote
) -> None:
"""Initialize SwitchBot Cloud."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
self._api = api
self._device_id = device.device_id
self._should_poll = not isinstance(device, Remote)

async def _async_update_data(self) -> Status:
"""Fetch data from API endpoint."""
if not self._should_poll:
return None
try:
_LOGGER.debug("Refreshing %s", self._device_id)
async with timeout(10):
status: Status = await self._api.get_status(self._device_id)
_LOGGER.debug("Refreshing %s with %s", self._device_id, status)
return status
except CannotConnect as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
49 changes: 49 additions & 0 deletions homeassistant/components/switchbot_cloud/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Base class for SwitchBot via API entities."""
from typing import Any

from switchbot_api import Commands, Device, Remote, SwitchBotAPI

from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
from .coordinator import SwitchBotCoordinator


class SwitchBotCloudEntity(CoordinatorEntity[SwitchBotCoordinator]):
"""Representation of a SwitchBot Cloud entity."""

_api: SwitchBotAPI
_switchbot_state: dict[str, Any] | None = None
_attr_has_entity_name = True

def __init__(
self,
api: SwitchBotAPI,
device: Device | Remote,
coordinator: SwitchBotCoordinator,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._api = api
self._attr_unique_id = device.device_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.device_id)},
name=device.device_name,
manufacturer="SwitchBot",
model=device.device_type,
)

async def send_command(
self,
command: Commands,
command_type: str = "command",
parameters: dict | str = "default",
) -> None:
"""Send command to device."""
await self._api.send_command(
self._attr_unique_id,
command,
command_type,
parameters,
)
10 changes: 10 additions & 0 deletions homeassistant/components/switchbot_cloud/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"domain": "switchbot_cloud",
"name": "SwitchBot Cloud",
"codeowners": ["@SeraphicRav"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/switchbot_cloud",
"iot_class": "cloud_polling",
"loggers": ["switchbot-api"],
"requirements": ["switchbot-api==1.1.0"]
}
20 changes: 20 additions & 0 deletions homeassistant/components/switchbot_cloud/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"config": {
"step": {
"user": {
"data": {
"api_token": "[%key:common::config_flow::data::api_token%]",
"api_key": "[%key:common::config_flow::data::api_key%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}