mirror of
https://github.com/home-assistant/core.git
synced 2026-02-15 07:36:16 +00:00
Switchbot Cloud: Add new supported device Ai Art Frame (#160754)
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
@@ -33,6 +33,7 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.COVER,
|
||||
Platform.FAN,
|
||||
Platform.HUMIDIFIER,
|
||||
Platform.IMAGE,
|
||||
Platform.LIGHT,
|
||||
Platform.LOCK,
|
||||
Platform.SENSOR,
|
||||
@@ -62,6 +63,7 @@ class SwitchbotDevices:
|
||||
fans: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
|
||||
lights: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
|
||||
humidifiers: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
|
||||
images: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -307,6 +309,14 @@ async def make_device_data(
|
||||
devices_data.binary_sensors.append((device, coordinator))
|
||||
devices_data.sensors.append((device, coordinator))
|
||||
|
||||
if isinstance(device, Device) and device.device_type == "AI Art Frame":
|
||||
coordinator = await coordinator_for_device(
|
||||
hass, entry, api, device, coordinators_by_id
|
||||
)
|
||||
devices_data.buttons.append((device, coordinator))
|
||||
devices_data.sensors.append((device, coordinator))
|
||||
devices_data.images.append((device, coordinator))
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up SwitchBot via API from a config entry."""
|
||||
|
||||
@@ -1,19 +1,61 @@
|
||||
"""Support for the Switchbot Bot as a Button."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from switchbot_api import BotCommands
|
||||
from switchbot_api import (
|
||||
Commands as SwitchBotCloudBaseCommands,
|
||||
Device,
|
||||
Remote,
|
||||
SwitchBotAPI,
|
||||
)
|
||||
from switchbot_api.commands import ArtFrameCommands, BotCommands, CommonCommands
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudData
|
||||
from . import SwitchbotCloudData, SwitchBotCoordinator
|
||||
from .const import DOMAIN
|
||||
from .entity import SwitchBotCloudEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SwitchbotCloudButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Switchbot Cloud Button EntityDescription."""
|
||||
|
||||
command: SwitchBotCloudBaseCommands = CommonCommands.PRESS
|
||||
command_type: str = "command"
|
||||
parameters: dict | str = "default"
|
||||
|
||||
|
||||
BOT_BUTTON_DESCRIPTION = SwitchbotCloudButtonEntityDescription(
|
||||
key="Button", command=BotCommands.PRESS, name=None
|
||||
)
|
||||
|
||||
ART_FRAME_NEXT_BUTTON_DESCRIPTION = SwitchbotCloudButtonEntityDescription(
|
||||
key="next",
|
||||
translation_key="art_frame_next_picture",
|
||||
command=ArtFrameCommands.NEXT,
|
||||
)
|
||||
|
||||
ART_FRAME_PREVIOUS_BUTTON_DESCRIPTION = SwitchbotCloudButtonEntityDescription(
|
||||
key="previous",
|
||||
translation_key="art_frame_previous_picture",
|
||||
command=ArtFrameCommands.PREVIOUS,
|
||||
)
|
||||
|
||||
|
||||
BUTTON_DESCRIPTIONS_BY_DEVICE_TYPES = {
|
||||
"Bot": (BOT_BUTTON_DESCRIPTION,),
|
||||
"AI Art Frame": (
|
||||
ART_FRAME_NEXT_BUTTON_DESCRIPTION,
|
||||
ART_FRAME_PREVIOUS_BUTTON_DESCRIPTION,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
@@ -21,21 +63,52 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up SwitchBot Cloud entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
|
||||
async_add_entities(
|
||||
SwitchBotCloudBot(data.api, device, coordinator)
|
||||
for device, coordinator in data.devices.buttons
|
||||
entities: list[SwitchBotCloudBot] = []
|
||||
for device, coordinator in data.devices.buttons:
|
||||
description_set = BUTTON_DESCRIPTIONS_BY_DEVICE_TYPES[device.device_type]
|
||||
for description in description_set:
|
||||
entities.extend(
|
||||
[_async_make_entity(data.api, device, coordinator, description)]
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class SwitchBotCloudBot(SwitchBotCloudEntity, ButtonEntity):
|
||||
"""Representation of a SwitchBot Bot."""
|
||||
|
||||
_attr_name = None
|
||||
entity_description: SwitchbotCloudButtonEntityDescription
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
def __init__(
|
||||
self,
|
||||
api: SwitchBotAPI,
|
||||
device: Device,
|
||||
coordinator: SwitchBotCoordinator,
|
||||
description: SwitchbotCloudButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize SwitchBot Cloud Button entity."""
|
||||
|
||||
super().__init__(api, device, coordinator)
|
||||
self.entity_description = description
|
||||
if description.key != "Button":
|
||||
self._attr_unique_id = f"{device.device_id}-{description.key}"
|
||||
self._device_id = device.device_id
|
||||
|
||||
async def async_press(self, **kwargs: Any) -> None:
|
||||
"""Bot press command."""
|
||||
await self.send_api_command(BotCommands.PRESS)
|
||||
"""Button press command."""
|
||||
await self._api.send_command(
|
||||
self._device_id,
|
||||
self.entity_description.command.value,
|
||||
self.entity_description.command_type,
|
||||
self.entity_description.parameters,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_make_entity(
|
||||
api: SwitchBotAPI,
|
||||
device: Device | Remote,
|
||||
coordinator: SwitchBotCoordinator,
|
||||
description: SwitchbotCloudButtonEntityDescription,
|
||||
) -> SwitchBotCloudBot:
|
||||
"""Make a button entity."""
|
||||
return SwitchBotCloudBot(api, device, coordinator, description)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
{
|
||||
"entity": {
|
||||
"button": {
|
||||
"art_frame_next_picture": {
|
||||
"default": "mdi:chevron-right-box"
|
||||
},
|
||||
"art_frame_previous_picture": {
|
||||
"default": "mdi:chevron-left-box"
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"air_purifier": {
|
||||
"default": "mdi:air-purifier",
|
||||
|
||||
71
homeassistant/components/switchbot_cloud/image.py
Normal file
71
homeassistant/components/switchbot_cloud/image.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Support for the Switchbot Image."""
|
||||
|
||||
import datetime
|
||||
|
||||
from switchbot_api import Device, Remote, SwitchBotAPI
|
||||
from switchbot_api.utils import get_file_stream_from_cloud
|
||||
|
||||
from homeassistant.components.image import ImageEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudData, SwitchBotCoordinator
|
||||
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(
|
||||
_async_make_entity(data.api, device, coordinator)
|
||||
for device, coordinator in data.devices.images
|
||||
)
|
||||
|
||||
|
||||
class SwitchBotCloudImage(SwitchBotCloudEntity, ImageEntity):
|
||||
"""Base Class for SwitchBot Image."""
|
||||
|
||||
_attr_translation_key = "display"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api: SwitchBotAPI,
|
||||
device: Device | Remote,
|
||||
coordinator: SwitchBotCoordinator,
|
||||
) -> None:
|
||||
"""Initialize the image entity."""
|
||||
super().__init__(api, device, coordinator)
|
||||
ImageEntity.__init__(self, self.coordinator.hass)
|
||||
self._image_content = b""
|
||||
|
||||
async def async_image(self) -> bytes | None:
|
||||
"""Async image."""
|
||||
if (
|
||||
not isinstance(self._attr_image_url, str)
|
||||
or len(self._attr_image_url.strip()) == 0
|
||||
):
|
||||
self._image_content = b""
|
||||
return None
|
||||
self._image_content = await get_file_stream_from_cloud(self._attr_image_url, 5)
|
||||
return self._image_content
|
||||
|
||||
def _set_attributes(self) -> None:
|
||||
"""Set attributes from coordinator data."""
|
||||
if self.coordinator.data is None:
|
||||
return
|
||||
self._attr_image_last_updated = datetime.datetime.now()
|
||||
self._attr_image_url = self.coordinator.data.get("imageUrl")
|
||||
|
||||
|
||||
@callback
|
||||
def _async_make_entity(
|
||||
api: SwitchBotAPI, device: Device | Remote, coordinator: SwitchBotCoordinator
|
||||
) -> SwitchBotCloudImage:
|
||||
"""Make a SwitchBotCloudImage."""
|
||||
return SwitchBotCloudImage(api, device, coordinator)
|
||||
@@ -253,6 +253,7 @@ SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
|
||||
BATTERY_DESCRIPTION,
|
||||
),
|
||||
"Smart Radiator Thermostat": (BATTERY_DESCRIPTION,),
|
||||
"AI Art Frame": (BATTERY_DESCRIPTION,),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,14 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"art_frame_next_picture": {
|
||||
"name": "Next"
|
||||
},
|
||||
"art_frame_previous_picture": {
|
||||
"name": "Previous"
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"air_purifier": {
|
||||
"state_attributes": {
|
||||
@@ -48,6 +56,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"display": {
|
||||
"name": "Display"
|
||||
}
|
||||
},
|
||||
|
||||
"sensor": {
|
||||
"light_level": {
|
||||
"name": "Light level"
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from switchbot_api import BotCommands, Device
|
||||
from switchbot_api.commands import ArtFrameCommands
|
||||
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||
from homeassistant.components.switchbot_cloud import SwitchBotAPI
|
||||
@@ -40,7 +42,7 @@ async def test_pressmode_bot(
|
||||
BUTTON_DOMAIN, SERVICE_PRESS, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
mock_send_command.assert_called_once_with(
|
||||
"bot-id-1", BotCommands.PRESS, "command", "default"
|
||||
"bot-id-1", BotCommands.PRESS.value, "command", "default"
|
||||
)
|
||||
|
||||
assert hass.states.get(entity_id).state != STATE_UNKNOWN
|
||||
@@ -65,3 +67,43 @@ async def test_switchmode_bot_no_button_entity(
|
||||
entry = await configure_integration(hass)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
assert not hass.states.async_entity_ids(BUTTON_DOMAIN)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"buttons",
|
||||
[
|
||||
("AI Art Frame", ArtFrameCommands.PREVIOUS),
|
||||
("AI Art Frame", ArtFrameCommands.NEXT),
|
||||
],
|
||||
)
|
||||
async def test_loaded_buttons(
|
||||
hass: HomeAssistant, mock_list_devices, mock_get_status, buttons
|
||||
) -> None:
|
||||
"""Test press."""
|
||||
mock_list_devices.return_value = [
|
||||
Device(
|
||||
version="V1.0",
|
||||
deviceId="device_id",
|
||||
deviceName="device_name",
|
||||
deviceType=buttons[0],
|
||||
hubDeviceId="test-hub-id",
|
||||
),
|
||||
]
|
||||
|
||||
mock_get_status.return_value = {
|
||||
"deviceType": buttons[0],
|
||||
}
|
||||
|
||||
entry = await configure_integration(hass)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity_id = f"button.device_name_{buttons[1].value}"
|
||||
assert hass.states.get(entity_id).state == STATE_UNKNOWN
|
||||
|
||||
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN, SERVICE_PRESS, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
mock_send_command.assert_called()
|
||||
|
||||
assert hass.states.get(entity_id).state != STATE_UNKNOWN
|
||||
|
||||
84
tests/components/switchbot_cloud/test_image.py
Normal file
84
tests/components/switchbot_cloud/test_image.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Test for the switchbot_cloud image."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from switchbot_api import Device
|
||||
|
||||
from homeassistant.components.switchbot_cloud import DOMAIN
|
||||
from homeassistant.components.switchbot_cloud.image import SwitchBotCloudImage
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import 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="ai-art-frame-id-1",
|
||||
deviceName="ai-art-frame-1",
|
||||
deviceType="AI Art Frame",
|
||||
hubDeviceId="test-hub-id",
|
||||
),
|
||||
]
|
||||
mock_get_status.side_effect = [None, None]
|
||||
entry = await configure_integration(hass)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
entity_id = "image.ai_art_frame_1_display"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state is STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_async_image(
|
||||
hass: HomeAssistant, mock_list_devices, mock_get_status
|
||||
) -> None:
|
||||
"""Test coordinator data is none."""
|
||||
|
||||
mock_list_devices.return_value = [
|
||||
Device(
|
||||
version="V1.0",
|
||||
deviceId="ai-art-frame-id-1",
|
||||
deviceName="ai-art-frame-1",
|
||||
deviceType="AI Art Frame",
|
||||
hubDeviceId="test-hub-id",
|
||||
),
|
||||
]
|
||||
mock_get_status.side_effect = [
|
||||
{
|
||||
"deviceId": "B0E9FEA5D7F0",
|
||||
"deviceType": "AI Art Frame",
|
||||
"hubDeviceId": "B0E9FEA5D7F0",
|
||||
"battery": 0,
|
||||
"displayMode": 1,
|
||||
"imageUrl": "https://p3.itc.cn/images01/20231215/2f2db37e221c4ad3af575254c7769ca1.jpeg",
|
||||
"version": "V0.0-0.5",
|
||||
}
|
||||
]
|
||||
entry = await configure_integration(hass)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
cloud_data = hass.data[DOMAIN][entry.entry_id]
|
||||
device, coordinator = cloud_data.devices.images[0]
|
||||
image_entity = SwitchBotCloudImage(cloud_data.api, device, coordinator)
|
||||
|
||||
# 1. load before refresh
|
||||
await image_entity.async_image()
|
||||
assert image_entity._image_content == b""
|
||||
|
||||
# 2. load after refresh
|
||||
with patch(
|
||||
"homeassistant.components.switchbot_cloud.image.get_file_stream_from_cloud",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_get:
|
||||
mock_get.return_value = b"this is a bytes"
|
||||
image_entity._attr_image_url = (
|
||||
"https://p3.itc.cn/images01/20231215/2f2db37e221c4ad3af575254c7769ca1.jpeg"
|
||||
)
|
||||
await image_entity.async_image()
|
||||
assert image_entity._image_content == mock_get.return_value
|
||||
Reference in New Issue
Block a user