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

Migrate Tuya integration to new sharing SDK #109155

Merged
merged 14 commits into from
Jan 31, 2024
197 changes: 93 additions & 104 deletions homeassistant/components/tuya/__init__.py
Original file line number Diff line number Diff line change
@@ -1,119 +1,91 @@
"""Support for Tuya Smart devices."""
from __future__ import annotations

from typing import NamedTuple

import requests
from tuya_iot import (
AuthType,
TuyaDevice,
TuyaDeviceListener,
TuyaDeviceManager,
TuyaHomeManager,
TuyaOpenAPI,
TuyaOpenMQ,
import logging
from typing import Any, NamedTuple

from tuya_sharing import (
CustomerDevice,
Manager,
SharingDeviceListener,
SharingTokenListener,
)

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_COUNTRY_CODE, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.dispatcher import dispatcher_send

from .const import (
CONF_ACCESS_ID,
CONF_ACCESS_SECRET,
CONF_APP_TYPE,
CONF_AUTH_TYPE,
CONF_ENDPOINT,
CONF_TERMINAL_ID,
CONF_TOKEN_INFO,
CONF_USER_CODE,
DOMAIN,
LOGGER,
PLATFORMS,
TUYA_CLIENT_ID,
TUYA_DISCOVERY_NEW,
TUYA_HA_SIGNAL_UPDATE_ENTITY,
)

# Suppress logs from the library, it logs unneeded on error
logging.getLogger("tuya_sharing").setLevel(logging.CRITICAL)


class HomeAssistantTuyaData(NamedTuple):
"""Tuya data stored in the Home Assistant data object."""

device_listener: TuyaDeviceListener
device_manager: TuyaDeviceManager
home_manager: TuyaHomeManager
manager: Manager
listener: SharingDeviceListener


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Async setup hass config entry."""
hass.data.setdefault(DOMAIN, {})

auth_type = AuthType(entry.data[CONF_AUTH_TYPE])
api = TuyaOpenAPI(
endpoint=entry.data[CONF_ENDPOINT],
access_id=entry.data[CONF_ACCESS_ID],
access_secret=entry.data[CONF_ACCESS_SECRET],
auth_type=auth_type,
if CONF_APP_TYPE in entry.data:
raise ConfigEntryAuthFailed("Authentication failed. Please re-authenticate.")

token_listener = TokenListener(hass, entry)
manager = Manager(
TUYA_CLIENT_ID,
entry.data[CONF_USER_CODE],
entry.data[CONF_TERMINAL_ID],
entry.data[CONF_ENDPOINT],
entry.data[CONF_TOKEN_INFO],
token_listener,
)

api.set_dev_channel("hass")

try:
if auth_type == AuthType.CUSTOM:
response = await hass.async_add_executor_job(
api.connect, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]
)
else:
response = await hass.async_add_executor_job(
api.connect,
entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD],
entry.data[CONF_COUNTRY_CODE],
entry.data[CONF_APP_TYPE],
)
except requests.exceptions.RequestException as err:
raise ConfigEntryNotReady(err) from err

if response.get("success", False) is False:
raise ConfigEntryNotReady(response)

tuya_mq = TuyaOpenMQ(api)
tuya_mq.start()

device_ids: set[str] = set()
device_manager = TuyaDeviceManager(api, tuya_mq)
home_manager = TuyaHomeManager(api, tuya_mq, device_manager)
listener = DeviceListener(hass, device_manager, device_ids)
device_manager.add_device_listener(listener)

hass.data[DOMAIN][entry.entry_id] = HomeAssistantTuyaData(
device_listener=listener,
device_manager=device_manager,
home_manager=home_manager,
listener = DeviceListener(hass, manager)
manager.add_device_listener(listener)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantTuyaData(
manager=manager, listener=listener
)

# Get devices & clean up device entities
await hass.async_add_executor_job(home_manager.update_device_cache)
await cleanup_device_registry(hass, device_manager)
await hass.async_add_executor_job(manager.update_device_cache)
await cleanup_device_registry(hass, manager)

# Register known device IDs
device_registry = dr.async_get(hass)
for device in device_manager.device_map.values():
for device in manager.device_map.values():
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, device.id)},
manufacturer="Tuya",
name=device.name,
model=f"{device.product_name} (unsupported)",
)
device_ids.add(device.id)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# If the device does not register any entities, the device does not need to subscribe
# So the subscription is here
await hass.async_add_executor_job(manager.refresh_mq)
return True


async def cleanup_device_registry(
hass: HomeAssistant, device_manager: TuyaDeviceManager
) -> None:
async def cleanup_device_registry(hass: HomeAssistant, device_manager: Manager) -> None:
"""Remove deleted device registry entry if there are no remaining entities."""
device_registry = dr.async_get(hass)
for dev_id, device_entry in list(device_registry.devices.items()):
Expand All @@ -125,59 +97,44 @@ async def cleanup_device_registry(

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unloading the Tuya platforms."""
unload = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload:
hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id]
hass_data.device_manager.mq.stop()
hass_data.device_manager.remove_device_listener(hass_data.device_listener)

hass.data[DOMAIN].pop(entry.entry_id)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
tuya: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id]
if tuya.manager.mq is not None:
tuya.manager.mq.stop()
tuya.manager.remove_device_listener(tuya.listener)
await hass.async_add_executor_job(tuya.manager.unload)
del hass.data[DOMAIN][entry.entry_id]
return unload_ok

return unload


class DeviceListener(TuyaDeviceListener):
class DeviceListener(SharingDeviceListener):
"""Device Update Listener."""

def __init__(
self,
hass: HomeAssistant,
device_manager: TuyaDeviceManager,
device_ids: set[str],
manager: Manager,
) -> None:
"""Init DeviceListener."""
self.hass = hass
self.device_manager = device_manager
self.device_ids = device_ids
self.manager = manager

def update_device(self, device: TuyaDevice) -> None:
def update_device(self, device: CustomerDevice) -> None:
"""Update device status."""
if device.id in self.device_ids:
LOGGER.debug(
"Received update for device %s: %s",
device.id,
self.device_manager.device_map[device.id].status,
)
dispatcher_send(self.hass, f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{device.id}")

def add_device(self, device: TuyaDevice) -> None:
LOGGER.debug(
"Received update for device %s: %s",
device.id,
self.manager.device_map[device.id].status,
)
dispatcher_send(self.hass, f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{device.id}")

def add_device(self, device: CustomerDevice) -> None:
"""Add device added listener."""
# Ensure the device isn't present stale
self.hass.add_job(self.async_remove_device, device.id)

self.device_ids.add(device.id)
frenck marked this conversation as resolved.
Show resolved Hide resolved
dispatcher_send(self.hass, TUYA_DISCOVERY_NEW, [device.id])

device_manager = self.device_manager
device_manager.mq.stop()
tuya_mq = TuyaOpenMQ(device_manager.api)
tuya_mq.start()

device_manager.mq = tuya_mq
tuya_mq.add_message_listener(device_manager.on_message)

def remove_device(self, device_id: str) -> None:
"""Add device removed listener."""
self.hass.add_job(self.async_remove_device, device_id)
Expand All @@ -192,4 +149,36 @@ def async_remove_device(self, device_id: str) -> None:
)
if device_entry is not None:
device_registry.async_remove_device(device_entry.id)
self.device_ids.discard(device_id)


class TokenListener(SharingTokenListener):
"""Token listener for upstream token updates."""

def __init__(
self,
hass: HomeAssistant,
entry: ConfigEntry,
) -> None:
"""Init TokenListener."""
self.hass = hass
self.entry = entry

def update_token(self, token_info: dict[str, Any]) -> None:
"""Update token info in config entry."""
data = {
**self.entry.data,
CONF_TOKEN_INFO: {
"t": token_info["t"],
"uid": token_info["uid"],
"expire_time": token_info["expire_time"],
"access_token": token_info["access_token"],
"refresh_token": token_info["refresh_token"],
},
}

@callback
def async_update_entry() -> None:
"""Update config entry."""
self.hass.config_entries.async_update_entry(self.entry, data=data)

self.hass.add_job(async_update_entry)
14 changes: 6 additions & 8 deletions homeassistant/components/tuya/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from enum import StrEnum

from tuya_iot import TuyaDevice, TuyaDeviceManager
from tuya_sharing import CustomerDevice, Manager

from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
Expand Down Expand Up @@ -68,18 +68,16 @@ def async_discover_device(device_ids: list[str]) -> None:
"""Discover and add a discovered Tuya siren."""
entities: list[TuyaAlarmEntity] = []
for device_id in device_ids:
device = hass_data.device_manager.device_map[device_id]
device = hass_data.manager.device_map[device_id]
if descriptions := ALARM.get(device.category):
for description in descriptions:
if description.key in device.status:
entities.append(
TuyaAlarmEntity(
device, hass_data.device_manager, description
)
TuyaAlarmEntity(device, hass_data.manager, description)
)
async_add_entities(entities)

async_discover_device([*hass_data.device_manager.device_map])
async_discover_device([*hass_data.manager.device_map])

entry.async_on_unload(
async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device)
Expand All @@ -94,8 +92,8 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity):

def __init__(
self,
device: TuyaDevice,
device_manager: TuyaDeviceManager,
device: CustomerDevice,
device_manager: Manager,
description: AlarmControlPanelEntityDescription,
) -> None:
"""Init Tuya Alarm."""
Expand Down
6 changes: 4 additions & 2 deletions homeassistant/components/tuya/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import struct
from typing import Any, Literal, Self, overload

from tuya_iot import TuyaDevice, TuyaDeviceManager
from tuya_sharing import CustomerDevice, Manager

from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
Expand Down Expand Up @@ -135,9 +135,11 @@ class TuyaEntity(Entity):
_attr_has_entity_name = True
_attr_should_poll = False

def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None:
def __init__(self, device: CustomerDevice, device_manager: Manager) -> None:
"""Init TuyaHaEntity."""
self._attr_unique_id = f"tuya.{device.id}"
# TuyaEntity initialize mq can subscribe
device.set_up = True
self.device = device
self.device_manager = device_manager

Expand Down
12 changes: 6 additions & 6 deletions homeassistant/components/tuya/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from dataclasses import dataclass

from tuya_iot import TuyaDevice, TuyaDeviceManager
from tuya_sharing import CustomerDevice, Manager

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
Expand Down Expand Up @@ -354,20 +354,20 @@ def async_discover_device(device_ids: list[str]) -> None:
"""Discover and add a discovered Tuya binary sensor."""
entities: list[TuyaBinarySensorEntity] = []
for device_id in device_ids:
device = hass_data.device_manager.device_map[device_id]
device = hass_data.manager.device_map[device_id]
if descriptions := BINARY_SENSORS.get(device.category):
for description in descriptions:
dpcode = description.dpcode or description.key
if dpcode in device.status:
entities.append(
TuyaBinarySensorEntity(
device, hass_data.device_manager, description
device, hass_data.manager, description
)
)

async_add_entities(entities)

async_discover_device([*hass_data.device_manager.device_map])
async_discover_device([*hass_data.manager.device_map])

entry.async_on_unload(
async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device)
Expand All @@ -381,8 +381,8 @@ class TuyaBinarySensorEntity(TuyaEntity, BinarySensorEntity):

def __init__(
self,
device: TuyaDevice,
device_manager: TuyaDeviceManager,
device: CustomerDevice,
device_manager: Manager,
description: TuyaBinarySensorEntityDescription,
) -> None:
"""Init Tuya binary sensor."""
Expand Down