mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Add light platform to switchbot (#77430)
This commit is contained in:
parent
40e8979951
commit
795691038f
@ -1205,6 +1205,7 @@ omit =
|
|||||||
homeassistant/components/switchbot/const.py
|
homeassistant/components/switchbot/const.py
|
||||||
homeassistant/components/switchbot/entity.py
|
homeassistant/components/switchbot/entity.py
|
||||||
homeassistant/components/switchbot/cover.py
|
homeassistant/components/switchbot/cover.py
|
||||||
|
homeassistant/components/switchbot/light.py
|
||||||
homeassistant/components/switchbot/sensor.py
|
homeassistant/components/switchbot/sensor.py
|
||||||
homeassistant/components/switchbot/coordinator.py
|
homeassistant/components/switchbot/coordinator.py
|
||||||
homeassistant/components/switchmate/switch.py
|
homeassistant/components/switchmate/switch.py
|
||||||
|
@ -23,12 +23,14 @@ from .const import (
|
|||||||
CONNECTABLE_SUPPORTED_MODEL_TYPES,
|
CONNECTABLE_SUPPORTED_MODEL_TYPES,
|
||||||
DEFAULT_RETRY_COUNT,
|
DEFAULT_RETRY_COUNT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL,
|
||||||
SupportedModels,
|
SupportedModels,
|
||||||
)
|
)
|
||||||
from .coordinator import SwitchbotDataUpdateCoordinator
|
from .coordinator import SwitchbotDataUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS_BY_TYPE = {
|
PLATFORMS_BY_TYPE = {
|
||||||
SupportedModels.BULB.value: [Platform.SENSOR],
|
SupportedModels.BULB.value: [Platform.SENSOR, Platform.LIGHT],
|
||||||
|
SupportedModels.LIGHT_STRIP.value: [Platform.SENSOR, Platform.LIGHT],
|
||||||
SupportedModels.BOT.value: [Platform.SWITCH, Platform.SENSOR],
|
SupportedModels.BOT.value: [Platform.SWITCH, Platform.SENSOR],
|
||||||
SupportedModels.PLUG.value: [Platform.SWITCH, Platform.SENSOR],
|
SupportedModels.PLUG.value: [Platform.SWITCH, Platform.SENSOR],
|
||||||
SupportedModels.CURTAIN.value: [
|
SupportedModels.CURTAIN.value: [
|
||||||
@ -44,6 +46,8 @@ CLASS_BY_DEVICE = {
|
|||||||
SupportedModels.CURTAIN.value: switchbot.SwitchbotCurtain,
|
SupportedModels.CURTAIN.value: switchbot.SwitchbotCurtain,
|
||||||
SupportedModels.BOT.value: switchbot.Switchbot,
|
SupportedModels.BOT.value: switchbot.Switchbot,
|
||||||
SupportedModels.PLUG.value: switchbot.SwitchbotPlugMini,
|
SupportedModels.PLUG.value: switchbot.SwitchbotPlugMini,
|
||||||
|
SupportedModels.BULB.value: switchbot.SwitchbotBulb,
|
||||||
|
SupportedModels.LIGHT_STRIP.value: switchbot.SwitchbotLightStrip,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -72,8 +76,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
sensor_type: str = entry.data[CONF_SENSOR_TYPE]
|
sensor_type: str = entry.data[CONF_SENSOR_TYPE]
|
||||||
|
switchbot_model = HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL[sensor_type]
|
||||||
# connectable means we can make connections to the device
|
# connectable means we can make connections to the device
|
||||||
connectable = sensor_type in CONNECTABLE_SUPPORTED_MODEL_TYPES.values()
|
connectable = switchbot_model in CONNECTABLE_SUPPORTED_MODEL_TYPES
|
||||||
address: str = entry.data[CONF_ADDRESS]
|
address: str = entry.data[CONF_ADDRESS]
|
||||||
ble_device = bluetooth.async_ble_device_from_address(
|
ble_device = bluetooth.async_ble_device_from_address(
|
||||||
hass, address.upper(), connectable
|
hass, address.upper(), connectable
|
||||||
@ -97,6 +102,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
entry.unique_id,
|
entry.unique_id,
|
||||||
entry.data.get(CONF_NAME, entry.title),
|
entry.data.get(CONF_NAME, entry.title),
|
||||||
connectable,
|
connectable,
|
||||||
|
switchbot_model,
|
||||||
)
|
)
|
||||||
entry.async_on_unload(coordinator.async_start())
|
entry.async_on_unload(coordinator.async_start())
|
||||||
if not await coordinator.async_wait_ready():
|
if not await coordinator.async_wait_ready():
|
||||||
|
@ -18,6 +18,7 @@ class SupportedModels(StrEnum):
|
|||||||
BULB = "bulb"
|
BULB = "bulb"
|
||||||
CURTAIN = "curtain"
|
CURTAIN = "curtain"
|
||||||
HYGROMETER = "hygrometer"
|
HYGROMETER = "hygrometer"
|
||||||
|
LIGHT_STRIP = "light_strip"
|
||||||
CONTACT = "contact"
|
CONTACT = "contact"
|
||||||
PLUG = "plug"
|
PLUG = "plug"
|
||||||
MOTION = "motion"
|
MOTION = "motion"
|
||||||
@ -28,6 +29,7 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = {
|
|||||||
SwitchbotModel.CURTAIN: SupportedModels.CURTAIN,
|
SwitchbotModel.CURTAIN: SupportedModels.CURTAIN,
|
||||||
SwitchbotModel.PLUG_MINI: SupportedModels.PLUG,
|
SwitchbotModel.PLUG_MINI: SupportedModels.PLUG,
|
||||||
SwitchbotModel.COLOR_BULB: SupportedModels.BULB,
|
SwitchbotModel.COLOR_BULB: SupportedModels.BULB,
|
||||||
|
SwitchbotModel.LIGHT_STRIP: SupportedModels.LIGHT_STRIP,
|
||||||
}
|
}
|
||||||
|
|
||||||
NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = {
|
NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = {
|
||||||
@ -36,9 +38,13 @@ NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = {
|
|||||||
SwitchbotModel.MOTION_SENSOR: SupportedModels.MOTION,
|
SwitchbotModel.MOTION_SENSOR: SupportedModels.MOTION,
|
||||||
}
|
}
|
||||||
|
|
||||||
SUPPORTED_MODEL_TYPES = {
|
SUPPORTED_MODEL_TYPES = (
|
||||||
**CONNECTABLE_SUPPORTED_MODEL_TYPES,
|
CONNECTABLE_SUPPORTED_MODEL_TYPES | NON_CONNECTABLE_SUPPORTED_MODEL_TYPES
|
||||||
**NON_CONNECTABLE_SUPPORTED_MODEL_TYPES,
|
)
|
||||||
|
|
||||||
|
|
||||||
|
HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL = {
|
||||||
|
str(v): k for k, v in SUPPORTED_MODEL_TYPES.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Config Defaults
|
# Config Defaults
|
||||||
|
@ -21,6 +21,8 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEVICE_STARTUP_TIMEOUT = 30
|
||||||
|
|
||||||
|
|
||||||
def flatten_sensors_data(sensor):
|
def flatten_sensors_data(sensor):
|
||||||
"""Deconstruct SwitchBot library temp object C/Fº readings from dictionary."""
|
"""Deconstruct SwitchBot library temp object C/Fº readings from dictionary."""
|
||||||
@ -42,6 +44,7 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
|||||||
base_unique_id: str,
|
base_unique_id: str,
|
||||||
device_name: str,
|
device_name: str,
|
||||||
connectable: bool,
|
connectable: bool,
|
||||||
|
model: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize global switchbot data updater."""
|
"""Initialize global switchbot data updater."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@ -56,6 +59,7 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
|||||||
self.data: dict[str, Any] = {}
|
self.data: dict[str, Any] = {}
|
||||||
self.device_name = device_name
|
self.device_name = device_name
|
||||||
self.base_unique_id = base_unique_id
|
self.base_unique_id = base_unique_id
|
||||||
|
self.model = model
|
||||||
self._ready_event = asyncio.Event()
|
self._ready_event = asyncio.Event()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -65,7 +69,6 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
|||||||
change: bluetooth.BluetoothChange,
|
change: bluetooth.BluetoothChange,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle a Bluetooth event."""
|
"""Handle a Bluetooth event."""
|
||||||
super()._async_handle_bluetooth_event(service_info, change)
|
|
||||||
if adv := switchbot.parse_advertisement_data(
|
if adv := switchbot.parse_advertisement_data(
|
||||||
service_info.device, service_info.advertisement
|
service_info.device, service_info.advertisement
|
||||||
):
|
):
|
||||||
@ -74,12 +77,12 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
|||||||
self._ready_event.set()
|
self._ready_event.set()
|
||||||
_LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data)
|
_LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data)
|
||||||
self.device.update_from_advertisement(adv)
|
self.device.update_from_advertisement(adv)
|
||||||
self.async_update_listeners()
|
super()._async_handle_bluetooth_event(service_info, change)
|
||||||
|
|
||||||
async def async_wait_ready(self) -> bool:
|
async def async_wait_ready(self) -> bool:
|
||||||
"""Wait for the device to be ready."""
|
"""Wait for the device to be ready."""
|
||||||
with contextlib.suppress(asyncio.TimeoutError):
|
with contextlib.suppress(asyncio.TimeoutError):
|
||||||
async with async_timeout.timeout(55):
|
async with async_timeout.timeout(DEVICE_STARTUP_TIMEOUT):
|
||||||
await self._ready_event.wait()
|
await self._ready_event.wait()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
"""An abstract class common to all Switchbot entities."""
|
"""An abstract class common to all Switchbot entities."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import abstractmethod
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from switchbot import SwitchbotDevice
|
||||||
|
|
||||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||||
PassiveBluetoothCoordinatorEntity,
|
PassiveBluetoothCoordinatorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_CONNECTIONS
|
from homeassistant.const import ATTR_CONNECTIONS
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
|
|
||||||
@ -19,6 +23,7 @@ class SwitchbotEntity(PassiveBluetoothCoordinatorEntity):
|
|||||||
"""Generic entity encapsulating common features of Switchbot device."""
|
"""Generic entity encapsulating common features of Switchbot device."""
|
||||||
|
|
||||||
coordinator: SwitchbotDataUpdateCoordinator
|
coordinator: SwitchbotDataUpdateCoordinator
|
||||||
|
_device: SwitchbotDevice
|
||||||
|
|
||||||
def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None:
|
def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
@ -31,7 +36,7 @@ class SwitchbotEntity(PassiveBluetoothCoordinatorEntity):
|
|||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
connections={(dr.CONNECTION_BLUETOOTH, self._address)},
|
connections={(dr.CONNECTION_BLUETOOTH, self._address)},
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
model=self.data["modelName"],
|
model=coordinator.model, # Sometimes the modelName is missing from the advertisement data
|
||||||
name=coordinator.device_name,
|
name=coordinator.device_name,
|
||||||
)
|
)
|
||||||
if ":" not in self._address:
|
if ":" not in self._address:
|
||||||
@ -54,3 +59,29 @@ class SwitchbotEntity(PassiveBluetoothCoordinatorEntity):
|
|||||||
def extra_state_attributes(self) -> Mapping[Any, Any]:
|
def extra_state_attributes(self) -> Mapping[Any, Any]:
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
return {"last_run_success": self._last_run_success}
|
return {"last_run_success": self._last_run_success}
|
||||||
|
|
||||||
|
|
||||||
|
class SwitchbotSubscribeEntity(SwitchbotEntity):
|
||||||
|
"""Base class for Switchbot entities that use subscribe."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update the entity attributes."""
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
"""Handle data update."""
|
||||||
|
self._async_update_attrs()
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Register callbacks."""
|
||||||
|
self.async_on_remove(self._device.subscribe(self._handle_coordinator_update))
|
||||||
|
return await super().async_added_to_hass()
|
||||||
|
|
||||||
|
async def async_update(self) -> None:
|
||||||
|
"""Update the entity.
|
||||||
|
|
||||||
|
Only used by the generic entity update service.
|
||||||
|
"""
|
||||||
|
await self._device.update()
|
||||||
|
98
homeassistant/components/switchbot/light.py
Normal file
98
homeassistant/components/switchbot/light.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
"""Switchbot integration light platform."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from switchbot import ColorMode as SwitchBotColorMode, SwitchbotBaseLight
|
||||||
|
|
||||||
|
from homeassistant.components.light import (
|
||||||
|
ATTR_BRIGHTNESS,
|
||||||
|
ATTR_COLOR_TEMP,
|
||||||
|
ATTR_RGB_COLOR,
|
||||||
|
ColorMode,
|
||||||
|
LightEntity,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.util.color import (
|
||||||
|
color_temperature_kelvin_to_mired,
|
||||||
|
color_temperature_mired_to_kelvin,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import SwitchbotDataUpdateCoordinator
|
||||||
|
from .entity import SwitchbotSubscribeEntity
|
||||||
|
|
||||||
|
SWITCHBOT_COLOR_MODE_TO_HASS = {
|
||||||
|
SwitchBotColorMode.RGB: ColorMode.RGB,
|
||||||
|
SwitchBotColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
|
||||||
|
}
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the switchbot light."""
|
||||||
|
coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
async_add_entities([SwitchbotLightEntity(coordinator)])
|
||||||
|
|
||||||
|
|
||||||
|
class SwitchbotLightEntity(SwitchbotSubscribeEntity, LightEntity):
|
||||||
|
"""Representation of switchbot light bulb."""
|
||||||
|
|
||||||
|
_device: SwitchbotBaseLight
|
||||||
|
|
||||||
|
def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None:
|
||||||
|
"""Initialize the Switchbot light."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
device = self._device
|
||||||
|
self._attr_min_mireds = color_temperature_kelvin_to_mired(device.max_temp)
|
||||||
|
self._attr_max_mireds = color_temperature_kelvin_to_mired(device.min_temp)
|
||||||
|
self._attr_supported_color_modes = {
|
||||||
|
SWITCHBOT_COLOR_MODE_TO_HASS[mode] for mode in device.color_modes
|
||||||
|
}
|
||||||
|
self._async_update_attrs()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Handle updating _attr values."""
|
||||||
|
device = self._device
|
||||||
|
self._attr_is_on = self._device.on
|
||||||
|
self._attr_brightness = max(0, min(255, round(device.brightness * 2.55)))
|
||||||
|
if device.color_mode == SwitchBotColorMode.COLOR_TEMP:
|
||||||
|
self._attr_color_temp = color_temperature_kelvin_to_mired(device.color_temp)
|
||||||
|
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||||
|
return
|
||||||
|
self._attr_rgb_color = device.rgb
|
||||||
|
self._attr_color_mode = ColorMode.RGB
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Instruct the light to turn on."""
|
||||||
|
brightness = round(kwargs.get(ATTR_BRIGHTNESS, self.brightness) / 255 * 100)
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.supported_color_modes
|
||||||
|
and ColorMode.COLOR_TEMP in self.supported_color_modes
|
||||||
|
and ATTR_COLOR_TEMP in kwargs
|
||||||
|
):
|
||||||
|
color_temp = kwargs[ATTR_COLOR_TEMP]
|
||||||
|
kelvin = max(2700, min(6500, color_temperature_mired_to_kelvin(color_temp)))
|
||||||
|
await self._device.set_color_temp(brightness, kelvin)
|
||||||
|
return
|
||||||
|
if ATTR_RGB_COLOR in kwargs:
|
||||||
|
rgb = kwargs[ATTR_RGB_COLOR]
|
||||||
|
await self._device.set_rgb(brightness, rgb[0], rgb[1], rgb[2])
|
||||||
|
return
|
||||||
|
if ATTR_BRIGHTNESS in kwargs:
|
||||||
|
await self._device.set_brightness(brightness)
|
||||||
|
return
|
||||||
|
await self._device.turn_on()
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Instruct the light to turn off."""
|
||||||
|
await self._device.turn_off()
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "switchbot",
|
"domain": "switchbot",
|
||||||
"name": "SwitchBot",
|
"name": "SwitchBot",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
||||||
"requirements": ["PySwitchbot==0.18.14"],
|
"requirements": ["PySwitchbot==0.18.21"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["bluetooth"],
|
"dependencies": ["bluetooth"],
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
|
@ -37,7 +37,7 @@ PyRMVtransport==0.3.3
|
|||||||
PySocks==1.7.1
|
PySocks==1.7.1
|
||||||
|
|
||||||
# homeassistant.components.switchbot
|
# homeassistant.components.switchbot
|
||||||
PySwitchbot==0.18.14
|
PySwitchbot==0.18.21
|
||||||
|
|
||||||
# homeassistant.components.transport_nsw
|
# homeassistant.components.transport_nsw
|
||||||
PyTransportNSW==0.1.1
|
PyTransportNSW==0.1.1
|
||||||
|
@ -33,7 +33,7 @@ PyRMVtransport==0.3.3
|
|||||||
PySocks==1.7.1
|
PySocks==1.7.1
|
||||||
|
|
||||||
# homeassistant.components.switchbot
|
# homeassistant.components.switchbot
|
||||||
PySwitchbot==0.18.14
|
PySwitchbot==0.18.21
|
||||||
|
|
||||||
# homeassistant.components.transport_nsw
|
# homeassistant.components.transport_nsw
|
||||||
PyTransportNSW==0.1.1
|
PyTransportNSW==0.1.1
|
||||||
|
Loading…
x
Reference in New Issue
Block a user