Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7b9cbcc
Merge pull request #1 from home-assistant/dev
ollo69 Apr 2, 2020
7b1cabb
Merge pull request #2 from home-assistant/dev
ollo69 Apr 3, 2020
3dea427
Merge pull request #3 from home-assistant/dev
ollo69 Apr 4, 2020
f2699b3
Merge pull request #4 from home-assistant/dev
ollo69 Apr 11, 2020
ce86d50
Merge pull request #5 from home-assistant/dev
ollo69 Apr 27, 2020
bd0520a
Merge pull request #6 from home-assistant/dev
ollo69 Apr 29, 2020
9d7b4cd
Merge pull request #7 from home-assistant/dev
ollo69 Apr 29, 2020
4e4da3c
Merge pull request #8 from home-assistant/dev
ollo69 Apr 30, 2020
1c0952e
Merge pull request #9 from home-assistant/dev
ollo69 May 2, 2020
6c70e63
Merge pull request #10 from home-assistant/dev
ollo69 May 6, 2020
4187271
Merge pull request #11 from home-assistant/dev
ollo69 May 7, 2020
683c152
Added Tuya config flow
ollo69 May 9, 2020
85b21de
Added test config_flow
ollo69 May 9, 2020
2788f50
Fixed log error message
ollo69 May 9, 2020
99a8b97
Add test requirements
ollo69 May 9, 2020
64684ba
Lint Fix
ollo69 May 9, 2020
ad8fbad
Merge branch 'tuya-config-flow' of https://github.com/ollo69/core int…
ollo69 May 9, 2020
d76245f
Fix Black formatting
ollo69 May 9, 2020
a816565
Added pylint directive
ollo69 May 9, 2020
13f3aa0
Implementation requested changes
ollo69 May 10, 2020
0e20022
Update CodeOwners
ollo69 May 10, 2020
e531b22
Removed device registry cleanup
ollo69 May 10, 2020
cd5bbed
Force checks
ollo69 May 10, 2020
4747dd7
Force checks
ollo69 May 10, 2020
5a15b78
Fix implemetation
ollo69 May 10, 2020
a2d539a
Updating test
ollo69 May 10, 2020
a0fe4ae
Fix formatting
ollo69 May 10, 2020
6b2bbf7
Config Flow test fix
ollo69 May 10, 2020
f59586f
Fix formatting
ollo69 May 10, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ homeassistant/components/tradfri/* @ggravlingen
homeassistant/components/trafikverket_train/* @endor-force
homeassistant/components/transmission/* @engrbm87 @JPHutchins
homeassistant/components/tts/* @pvizeli
homeassistant/components/tuya/* @ollo69
homeassistant/components/twentemilieu/* @frenck
homeassistant/components/twilio_call/* @robbiet480
homeassistant/components/twilio_sms/* @robbiet480
Expand Down
217 changes: 147 additions & 70 deletions homeassistant/components/tuya/__init__.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,44 @@
"""Support for Tuya Smart devices."""
import asyncio
from datetime import timedelta
import logging

from tuyaha import TuyaApi
from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException, TuyaServerException
import voluptuous as vol

from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers import discovery
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import call_later, track_time_interval
from homeassistant.helpers.event import async_track_time_interval

from .const import (
CONF_COUNTRYCODE,
DOMAIN,
TUYA_DATA,
TUYA_DISCOVERY_NEW,
TUYA_PLATFORMS,
)

_LOGGER = logging.getLogger(__name__)

CONF_COUNTRYCODE = "country_code"
ENTRY_IS_SETUP = "tuya_entry_is_setup"

PARALLEL_UPDATES = 0

DOMAIN = "tuya"
DATA_TUYA = "data_tuya"

FIRST_RETRY_TIME = 60
MAX_RETRY_TIME = 900
SERVICE_FORCE_UPDATE = "force_update"
SERVICE_PULL_DEVICES = "pull_devices"

SIGNAL_DELETE_ENTITY = "tuya_delete"
SIGNAL_UPDATE_ENTITY = "tuya_update"

SERVICE_FORCE_UPDATE = "force_update"
SERVICE_PULL_DEVICES = "pull_devices"

TUYA_TYPE_TO_HA = {
"climate": "climate",
"cover": "cover",
Expand All @@ -41,59 +48,71 @@
"switch": "switch",
}

TUYA_TRACKER = "tuya_tracker"

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_COUNTRYCODE): cv.string,
vol.Optional(CONF_PLATFORM, default="tuya"): cv.string,
}
)
},
vol.All(
cv.deprecated(DOMAIN),
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_COUNTRYCODE): cv.string,
vol.Optional(CONF_PLATFORM, default="tuya"): cv.string,
}
)
},
),
extra=vol.ALLOW_EXTRA,
)


def setup(hass, config, retry_delay=FIRST_RETRY_TIME):
"""Set up Tuya Component."""
async def async_setup(hass, config):
"""Set up the Tuya integration."""

_LOGGER.debug("Setting up integration")
conf = config.get(DOMAIN)
if conf is not None:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
)
)

tuya = TuyaApi()
username = config[DOMAIN][CONF_USERNAME]
password = config[DOMAIN][CONF_PASSWORD]
country_code = config[DOMAIN][CONF_COUNTRYCODE]
platform = config[DOMAIN][CONF_PLATFORM]
return True

try:
tuya.init(username, password, country_code, platform)
except (TuyaNetException, TuyaServerException):

_LOGGER.warning(
"Connection error during integration setup. Will retry in %s seconds",
retry_delay,
)
async def async_setup_entry(hass, entry):
"""Set up Tuya platform."""

def retry_setup(now):
"""Retry setup if a error happens on tuya API."""
setup(hass, config, retry_delay=min(2 * retry_delay, MAX_RETRY_TIME))

call_later(hass, retry_delay, retry_setup)
tuya = TuyaApi()
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
country_code = entry.data[CONF_COUNTRYCODE]
platform = entry.data[CONF_PLATFORM]

return True
try:
await hass.async_add_executor_job(
tuya.init, username, password, country_code, platform
)
except (TuyaNetException, TuyaServerException):
raise ConfigEntryNotReady()

except TuyaAPIException as exc:
_LOGGER.error(
"Connection error during integration setup. Error: %s", exc,
)
return False

hass.data[DATA_TUYA] = tuya
hass.data[DOMAIN] = {"entities": {}}
hass.data[DOMAIN] = {
TUYA_DATA: tuya,
TUYA_TRACKER: None,
ENTRY_IS_SETUP: set(),
"entities": {},
"pending": {},
}

def load_devices(device_list):
async def async_load_devices(device_list):
"""Load new devices by device_list."""
device_type_list = {}
for device in device_list:
Expand All @@ -107,84 +126,142 @@ def load_devices(device_list):
device_type_list[ha_type] = []
device_type_list[ha_type].append(device.object_id())
hass.data[DOMAIN]["entities"][device.object_id()] = None
for ha_type, dev_ids in device_type_list.items():
discovery.load_platform(hass, ha_type, DOMAIN, {"dev_ids": dev_ids}, config)

device_list = tuya.get_all_devices()
load_devices(device_list)
for ha_type, dev_ids in device_type_list.items():
config_entries_key = f"{ha_type}.tuya"
if config_entries_key not in hass.data[DOMAIN][ENTRY_IS_SETUP]:
hass.data[DOMAIN]["pending"][ha_type] = dev_ids
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, ha_type)
)
hass.data[DOMAIN][ENTRY_IS_SETUP].add(config_entries_key)
else:
async_dispatcher_send(hass, TUYA_DISCOVERY_NEW.format(ha_type), dev_ids)

device_list = await hass.async_add_executor_job(tuya.get_all_devices)
await async_load_devices(device_list)

def _get_updated_devices():
tuya.poll_devices_update()
return tuya.get_all_devices()

def poll_devices_update(event_time):
async def async_poll_devices_update(event_time):
"""Check if accesstoken is expired and pull device list from server."""
_LOGGER.debug("Pull devices from Tuya.")
tuya.poll_devices_update()
# Add new discover device.
device_list = tuya.get_all_devices()
load_devices(device_list)
device_list = await hass.async_add_executor_job(_get_updated_devices)
await async_load_devices(device_list)
# Delete not exist device.
newlist_ids = []
for device in device_list:
newlist_ids.append(device.object_id())
for dev_id in list(hass.data[DOMAIN]["entities"]):
if dev_id not in newlist_ids:
dispatcher_send(hass, SIGNAL_DELETE_ENTITY, dev_id)
async_dispatcher_send(hass, SIGNAL_DELETE_ENTITY, dev_id)
hass.data[DOMAIN]["entities"].pop(dev_id)

track_time_interval(hass, poll_devices_update, timedelta(minutes=5))
hass.data[DOMAIN][TUYA_TRACKER] = async_track_time_interval(
hass, async_poll_devices_update, timedelta(minutes=5)
)

hass.services.register(DOMAIN, SERVICE_PULL_DEVICES, poll_devices_update)
hass.services.async_register(
DOMAIN, SERVICE_PULL_DEVICES, async_poll_devices_update
)

def force_update(call):
async def async_force_update(call):
"""Force all devices to pull data."""
dispatcher_send(hass, SIGNAL_UPDATE_ENTITY)
async_dispatcher_send(hass, SIGNAL_UPDATE_ENTITY)

hass.services.register(DOMAIN, SERVICE_FORCE_UPDATE, force_update)
hass.services.async_register(DOMAIN, SERVICE_FORCE_UPDATE, async_force_update)

return True


async def async_unload_entry(hass, entry):
"""Unloading the Tuya platforms."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(
entry, component.split(".", 1)[0]
)
for component in hass.data[DOMAIN][ENTRY_IS_SETUP]
]
)
)
if unload_ok:
hass.data[DOMAIN][ENTRY_IS_SETUP] = set()
hass.data[DOMAIN][TUYA_TRACKER]()
hass.data[DOMAIN][TUYA_TRACKER] = None
hass.data[DOMAIN][TUYA_DATA] = None
hass.services.async_remove(DOMAIN, SERVICE_FORCE_UPDATE)
hass.services.async_remove(DOMAIN, SERVICE_PULL_DEVICES)
hass.data.pop(DOMAIN)

return unload_ok


class TuyaDevice(Entity):
"""Tuya base device."""

def __init__(self, tuya):
def __init__(self, tuya, platform):
"""Init Tuya devices."""
self.tuya = tuya
self._tuya = tuya
self._platform = platform

async def async_added_to_hass(self):
"""Call when entity is added to hass."""
dev_id = self.tuya.object_id()
dev_id = self._tuya.object_id()
self.hass.data[DOMAIN]["entities"][dev_id] = self.entity_id
async_dispatcher_connect(self.hass, SIGNAL_DELETE_ENTITY, self._delete_callback)
async_dispatcher_connect(self.hass, SIGNAL_UPDATE_ENTITY, self._update_callback)
Comment thread
bdraco marked this conversation as resolved.

@property
def object_id(self):
"""Return Tuya device id."""
return self.tuya.object_id()
return self._tuya.object_id()

@property
def unique_id(self):
"""Return a unique ID."""
return f"tuya.{self.tuya.object_id()}"
return f"tuya.{self._tuya.object_id()}"

@property
def name(self):
"""Return Tuya device name."""
return self.tuya.name()
return self._tuya.name()

@property
def available(self):
"""Return if the device is available."""
return self.tuya.available()
return self._tuya.available()

@property
def device_info(self):
"""Return a device description for device registry."""
_device_info = {
"identifiers": {(DOMAIN, f"{self.unique_id}")},
"manufacturer": TUYA_PLATFORMS.get(self._platform, self._platform),
"name": self.name,
"model": self._tuya.object_type(),
}
return _device_info

def update(self):
"""Refresh Tuya device data."""
self.tuya.update()
self._tuya.update()

@callback
def _delete_callback(self, dev_id):
async def _delete_callback(self, dev_id):
Comment thread
ollo69 marked this conversation as resolved.
"""Remove this entity."""
if dev_id == self.object_id:
self.hass.async_create_task(self.async_remove())
entity_registry = (
await self.hass.helpers.entity_registry.async_get_registry()
)
if entity_registry.async_is_registered(self.entity_id):
entity_registry.async_remove(self.entity_id)
else:
await self.async_remove()

@callback
def _update_callback(self):
Expand Down
Loading