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

Tuya v2 integration New Update #53510

Merged
merged 60 commits into from
Sep 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
059c431
[init] tuya v2 stable version
tsutsuku Jul 26, 2021
c82734e
[add] translations
tsutsuku Jul 26, 2021
fbd3279
[update] update code by following the review suggestion.
tsutsuku Jul 27, 2021
1752dec
[update] change _LOGGER.info to debug
tsutsuku Jul 28, 2021
3b5531c
udpate: remove all but switch platform
dengweijun Aug 2, 2021
c147724
[udpate] modify references files .coveragec about tuya
dengweijun Aug 2, 2021
5600d0a
[update] fix warning
dengweijun Aug 3, 2021
8f29c18
[update] fix warning
dengweijun Aug 4, 2021
1828a47
[update] 'E722 do not use bare except'
dengweijun Aug 4, 2021
fe8415d
fix: F401 'sys' imported but unused
dengweijun Aug 4, 2021
aea763d
fix warning : Catching too general exception
dengweijun Aug 5, 2021
7ca4c64
fix: check isort
dengweijun Aug 6, 2021
f9ec990
fix: check isort
dengweijun Aug 6, 2021
791c9c0
Merge branch 'tuya-v2' of https://github.com/TuyaInc/core into tuya-v2
dengweijun Aug 6, 2021
7b4badc
code review modify
Aug 19, 2021
2421fbf
send_command should be async
Aug 20, 2021
eaebd9f
Merge pull request #1 from METISU/tuya-v2
tsutsuku Aug 23, 2021
019e0e7
remove line.
zlinoliver Aug 23, 2021
832020a
Delete platform dic, use TUYA_SUPPORT_HA_TYPE to maintain
Aug 25, 2021
11d6f42
codeReview modify
Aug 26, 2021
fb55817
convert TUYA_HA_DEVICES into a dictionary and user device_id as key t…
Aug 27, 2021
f3caf31
move DeviceListener outside
Aug 27, 2021
0ac70f7
code review
Aug 27, 2021
d5b65c5
delete log
Aug 27, 2021
659b7d0
correct the code rules
Aug 27, 2021
e059a13
Don't move directly to another steps form. A step is only allowed to …
Aug 28, 2021
69f2778
We shouldn't log the user input since it contains credentials.
Aug 28, 2021
a1a05ca
store the URL in a string constant
Aug 28, 2021
2c26c27
code Specification
Aug 28, 2021
13c01af
Decoupling step login, user input cannot be empty
Aug 28, 2021
2c9ca64
These files don't matter. They will be automatically deleted when the…
Aug 30, 2021
c0dd73d
It's not the responsibility of integrations to encrypt data
Aug 30, 2021
6f4fcf9
This shouldn't be needed
Aug 30, 2021
b2e9ae2
Merge pull request #2 from METISU/tuya-v2
tsutsuku Aug 31, 2021
1d8fb11
Don't store the entity in this shared container. Just store the devic…
Aug 31, 2021
2619344
Include the device id in this signal to let the dispatch helper look …
Aug 31, 2021
18e4499
Delete redundant comments
Sep 1, 2021
ee3d9cc
All the data stored for the domain in hass.data should be indexed fir…
Sep 1, 2021
f936f83
Please don't move to another step's form directly. If we need to go t…
Sep 1, 2021
934fbbf
Code Specification
Sep 1, 2021
9f2b768
Store the device id in a set in hass.data[DOMAIN][TUYA_HA_DEVICES] wh…
Sep 1, 2021
4e87cc3
Merge pull request #3 from METISU/tuya-v2
tsutsuku Sep 2, 2021
0c7d8b6
revert this change
Sep 2, 2021
d11caa1
The current Tuya integration doesn't have a YAML configuration. We sh…
Sep 2, 2021
9e5b109
sort this below __init__.py
Sep 2, 2021
3db3d04
change variable names that have double underscore as prefix
Sep 2, 2021
d89ef76
code review
Sep 2, 2021
0a33484
code review
Sep 2, 2021
8d1131e
unit testing adjustment
Sep 3, 2021
3d11a65
Merge pull request #4 from METISU/tuya-v2
tsutsuku Sep 3, 2021
3c04711
code review
Sep 6, 2021
31e4362
ci build errors
Sep 6, 2021
4b986f6
Merge pull request #5 from METISU/tuya-v2
tsutsuku Sep 6, 2021
5ce76c9
unit test optimization and crash protection
Sep 6, 2021
c86ed96
delete blank lines
Sep 6, 2021
5dbee34
Merge pull request #6 from METISU/tuya-v2
tsutsuku Sep 7, 2021
3cc9202
ci build errors
Sep 7, 2021
89b1c37
Merge pull request #7 from METISU/tuya-v2
zlinoliver Sep 7, 2021
b6e8378
code review
Sep 7, 2021
3342e5a
Merge pull request #8 from METISU/tuya-v2
zlinoliver Sep 7, 2021
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
6 changes: 1 addition & 5 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -1095,12 +1095,8 @@ omit =
homeassistant/components/transmission/errors.py
homeassistant/components/travisci/sensor.py
homeassistant/components/tuya/__init__.py
homeassistant/components/tuya/climate.py
homeassistant/components/tuya/base.py
homeassistant/components/tuya/const.py
homeassistant/components/tuya/cover.py
homeassistant/components/tuya/fan.py
homeassistant/components/tuya/light.py
homeassistant/components/tuya/scene.py
homeassistant/components/tuya/switch.py
homeassistant/components/twentemilieu/const.py
homeassistant/components/twentemilieu/sensor.py
Expand Down
2 changes: 1 addition & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ homeassistant/components/trafikverket_train/* @endor-force
homeassistant/components/trafikverket_weatherstation/* @endor-force
homeassistant/components/transmission/* @engrbm87 @JPHutchins
homeassistant/components/tts/* @pvizeli
homeassistant/components/tuya/* @ollo69
homeassistant/components/tuya/* @Tuya
homeassistant/components/twentemilieu/* @frenck
homeassistant/components/twinkly/* @dr1rrb
homeassistant/components/ubus/* @noltari
Expand Down
208 changes: 208 additions & 0 deletions homeassistant/components/tuya/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""Support for Tuya Smart devices."""

import itertools
import logging

from tuya_iot import (
ProjectType,
TuyaDevice,
TuyaDeviceListener,
TuyaDeviceManager,
TuyaHomeManager,
TuyaOpenAPI,
TuyaOpenMQ,
)

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry
from homeassistant.helpers.dispatcher import dispatcher_send

from .const import (
CONF_ACCESS_ID,
CONF_ACCESS_SECRET,
CONF_APP_TYPE,
CONF_COUNTRY_CODE,
CONF_ENDPOINT,
CONF_PASSWORD,
CONF_PROJECT_TYPE,
CONF_USERNAME,
DOMAIN,
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
PLATFORMS,
TUYA_DEVICE_MANAGER,
TUYA_DISCOVERY_NEW,
TUYA_HA_DEVICES,
TUYA_HA_SIGNAL_UPDATE_ENTITY,
TUYA_HA_TUYA_MAP,
TUYA_HOME_MANAGER,
TUYA_MQTT_LISTENER,
)

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Async setup hass config entry."""

_LOGGER.debug("tuya.__init__.async_setup_entry-->%s", entry.data)

hass.data[DOMAIN] = {entry.entry_id: {TUYA_HA_TUYA_MAP: {}, TUYA_HA_DEVICES: set()}}

success = await _init_tuya_sdk(hass, entry)
if not success:
return False

return True


async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool:
project_type = ProjectType(entry.data[CONF_PROJECT_TYPE])
api = TuyaOpenAPI(
entry.data[CONF_ENDPOINT],
entry.data[CONF_ACCESS_ID],
entry.data[CONF_ACCESS_SECRET],
project_type,
)

api.set_dev_channel("hass")

if project_type == ProjectType.INDUSTY_SOLUTIONS:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in the project name. This will need fixing upstream.

Suggested change
if project_type == ProjectType.INDUSTY_SOLUTIONS:
if project_type == ProjectType.INDUSTRY_SOLUTIONS:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not fixed yet.

response = await hass.async_add_executor_job(
api.login, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]
)
else:
response = await hass.async_add_executor_job(
api.login,
entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD],
entry.data[CONF_COUNTRY_CODE],
entry.data[CONF_APP_TYPE],
)

if response.get("success", False) is False:
_LOGGER.error("Tuya login error response: %s", response)
return False

tuya_mq = TuyaOpenMQ(api)
tuya_mq.start()
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved

device_manager = TuyaDeviceManager(api, tuya_mq)

# Get device list
home_manager = TuyaHomeManager(api, tuya_mq, device_manager)
await hass.async_add_executor_job(home_manager.update_device_cache)
hass.data[DOMAIN][entry.entry_id][TUYA_HOME_MANAGER] = home_manager

listener = DeviceListener(hass, entry)
hass.data[DOMAIN][entry.entry_id][TUYA_MQTT_LISTENER] = listener
device_manager.add_device_listener(listener)
hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] = device_manager

# Clean up device entities
await cleanup_device_registry(hass, entry)

_LOGGER.debug("init support type->%s", PLATFORMS)

hass.config_entries.async_setup_platforms(entry, PLATFORMS)

return True


async def cleanup_device_registry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Remove deleted device registry entry if there are no remaining entities."""

device_registry_object = device_registry.async_get(hass)
device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER]

for dev_id, device_entry in list(device_registry_object.devices.items()):
for item in device_entry.identifiers:
if DOMAIN == item[0] and item[1] not in device_manager.device_map:
device_registry_object.async_remove_device(dev_id)
break


@callback
def async_remove_hass_device(hass: HomeAssistant, device_id: str) -> None:
"""Remove device from hass cache."""
device_registry_object = device_registry.async_get(hass)
for device_entry in list(device_registry_object.devices.values()):
if device_id in list(device_entry.identifiers)[0]:
device_registry_object.async_remove_device(device_entry.id)


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

hass.data.pop(DOMAIN)

return unload


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

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

self.hass = hass
self.entry = entry

def update_device(self, device: TuyaDevice) -> None:
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
"""Update device status."""
if device.id in self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_DEVICES]:
_LOGGER.debug(
"_update-->%s;->>%s",
self,
device.id,
)
dispatcher_send(self.hass, f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{device.id}")

def add_device(self, device: TuyaDevice) -> None:
"""Add device added listener."""
device_add = False

if device.category in itertools.chain(
*self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_TUYA_MAP].values()
):
ha_tuya_map = self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_TUYA_MAP]
self.hass.add_job(async_remove_hass_device, self.hass, device.id)

for domain, tuya_list in ha_tuya_map.items():
if device.category in tuya_list:
device_add = True
_LOGGER.debug(
"Add device category->%s; domain-> %s",
device.category,
domain,
)
self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_DEVICES].add(
device.id
)
dispatcher_send(
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
self.hass, TUYA_DISCOVERY_NEW.format(domain), [device.id]
)

if device_add:
device_manager = self.hass.data[DOMAIN][self.entry.entry_id][
TUYA_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."""
_LOGGER.debug("tuya remove device:%s", device_id)
self.hass.add_job(async_remove_hass_device, self.hass, device_id)
76 changes: 76 additions & 0 deletions homeassistant/components/tuya/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Tuya Home Assistant Base Device Model."""
from __future__ import annotations

from typing import Any

from tuya_iot import TuyaDevice, TuyaDeviceManager

from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity

from .const import DOMAIN, TUYA_HA_SIGNAL_UPDATE_ENTITY


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

def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None:
"""Init TuyaHaEntity."""
super().__init__()

self.tuya_device = device
self.tuya_device_manager = device_manager

@staticmethod
def remap(old_value, old_min, old_max, new_min, new_max):
"""Remap old_value to new_value."""
new_value = ((old_value - old_min) / (old_max - old_min)) * (
new_max - new_min
) + new_min
return new_value

@property
def should_poll(self) -> bool:
"""Hass should not poll."""
return False

@property
def unique_id(self) -> str | None:
"""Return a unique ID."""
return f"tuya.{self.tuya_device.id}"
balloob marked this conversation as resolved.
Show resolved Hide resolved

@property
def name(self) -> str | None:
"""Return Tuya device name."""
return self.tuya_device.name

@property
def device_info(self):
"""Return a device description for device registry."""
_device_info = {
"identifiers": {(DOMAIN, f"{self.tuya_device.id}")},
"manufacturer": "Tuya",
"name": self.tuya_device.name,
"model": self.tuya_device.product_name,
}
return _device_info

@property
def available(self) -> bool:
"""Return if the device is available."""
return self.tuya_device.online

async def async_added_to_hass(self):
"""Call when entity is added to hass."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{self.tuya_device.id}",
self.async_write_ha_state,
)
)

def _send_command(self, commands: list[dict[str, Any]]) -> None:
"""Send command to the device."""
self.tuya_device_manager.send_commands(self.tuya_device.id, commands)
Loading