Update tuyaha to 0.0.8 and adapt code (#41375)
This commit is contained in:
parent
2858a90d5c
commit
c4b3cf0788
11 changed files with 776 additions and 51 deletions
|
@ -4,12 +4,17 @@ from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from tuyaha import TuyaApi
|
from tuyaha import TuyaApi
|
||||||
from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException, TuyaServerException
|
from tuyaha.tuyaapi import (
|
||||||
|
TuyaAPIException,
|
||||||
|
TuyaFrequentlyInvokeException,
|
||||||
|
TuyaNetException,
|
||||||
|
TuyaServerException,
|
||||||
|
)
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
|
@ -21,24 +26,30 @@ from homeassistant.helpers.event import async_track_time_interval
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_COUNTRYCODE,
|
CONF_COUNTRYCODE,
|
||||||
|
CONF_DISCOVERY_INTERVAL,
|
||||||
|
CONF_QUERY_DEVICE,
|
||||||
|
CONF_QUERY_INTERVAL,
|
||||||
|
DEFAULT_DISCOVERY_INTERVAL,
|
||||||
|
DEFAULT_QUERY_INTERVAL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
SIGNAL_CONFIG_ENTITY,
|
||||||
|
SIGNAL_DELETE_ENTITY,
|
||||||
|
SIGNAL_UPDATE_ENTITY,
|
||||||
TUYA_DATA,
|
TUYA_DATA,
|
||||||
|
TUYA_DEVICES_CONF,
|
||||||
TUYA_DISCOVERY_NEW,
|
TUYA_DISCOVERY_NEW,
|
||||||
TUYA_PLATFORMS,
|
TUYA_PLATFORMS,
|
||||||
|
TUYA_TYPE_NOT_QUERY,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ATTR_TUYA_DEV_ID = "tuya_device_id"
|
||||||
ENTRY_IS_SETUP = "tuya_entry_is_setup"
|
ENTRY_IS_SETUP = "tuya_entry_is_setup"
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
|
||||||
|
|
||||||
SERVICE_FORCE_UPDATE = "force_update"
|
SERVICE_FORCE_UPDATE = "force_update"
|
||||||
SERVICE_PULL_DEVICES = "pull_devices"
|
SERVICE_PULL_DEVICES = "pull_devices"
|
||||||
|
|
||||||
SIGNAL_DELETE_ENTITY = "tuya_delete"
|
|
||||||
SIGNAL_UPDATE_ENTITY = "tuya_update"
|
|
||||||
|
|
||||||
TUYA_TYPE_TO_HA = {
|
TUYA_TYPE_TO_HA = {
|
||||||
"climate": "climate",
|
"climate": "climate",
|
||||||
"cover": "cover",
|
"cover": "cover",
|
||||||
|
@ -56,9 +67,9 @@ CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
DOMAIN: vol.Schema(
|
DOMAIN: vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Required(CONF_COUNTRYCODE): cv.string,
|
vol.Required(CONF_COUNTRYCODE): cv.string,
|
||||||
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_PLATFORM, default="tuya"): cv.string,
|
vol.Optional(CONF_PLATFORM, default="tuya"): cv.string,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -68,6 +79,30 @@ CONFIG_SCHEMA = vol.Schema(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _update_discovery_interval(hass, interval):
|
||||||
|
tuya = hass.data[DOMAIN].get(TUYA_DATA)
|
||||||
|
if not tuya:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
tuya.discovery_interval = interval
|
||||||
|
_LOGGER.info("Tuya discovery device poll interval set to %s seconds", interval)
|
||||||
|
except ValueError as ex:
|
||||||
|
_LOGGER.warning(ex)
|
||||||
|
|
||||||
|
|
||||||
|
def _update_query_interval(hass, interval):
|
||||||
|
tuya = hass.data[DOMAIN].get(TUYA_DATA)
|
||||||
|
if not tuya:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
tuya.query_interval = interval
|
||||||
|
_LOGGER.info("Tuya query device poll interval set to %s seconds", interval)
|
||||||
|
except ValueError as ex:
|
||||||
|
_LOGGER.warning(ex)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up the Tuya integration."""
|
"""Set up the Tuya integration."""
|
||||||
|
|
||||||
|
@ -82,7 +117,7 @@ async def async_setup(hass, config):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass, entry):
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
"""Set up Tuya platform."""
|
"""Set up Tuya platform."""
|
||||||
|
|
||||||
tuya = TuyaApi()
|
tuya = TuyaApi()
|
||||||
|
@ -95,7 +130,11 @@ async def async_setup_entry(hass, entry):
|
||||||
await hass.async_add_executor_job(
|
await hass.async_add_executor_job(
|
||||||
tuya.init, username, password, country_code, platform
|
tuya.init, username, password, country_code, platform
|
||||||
)
|
)
|
||||||
except (TuyaNetException, TuyaServerException) as exc:
|
except (
|
||||||
|
TuyaNetException,
|
||||||
|
TuyaServerException,
|
||||||
|
TuyaFrequentlyInvokeException,
|
||||||
|
) as exc:
|
||||||
raise ConfigEntryNotReady() from exc
|
raise ConfigEntryNotReady() from exc
|
||||||
|
|
||||||
except TuyaAPIException as exc:
|
except TuyaAPIException as exc:
|
||||||
|
@ -107,12 +146,22 @@ async def async_setup_entry(hass, entry):
|
||||||
|
|
||||||
hass.data[DOMAIN] = {
|
hass.data[DOMAIN] = {
|
||||||
TUYA_DATA: tuya,
|
TUYA_DATA: tuya,
|
||||||
|
TUYA_DEVICES_CONF: entry.options.copy(),
|
||||||
TUYA_TRACKER: None,
|
TUYA_TRACKER: None,
|
||||||
ENTRY_IS_SETUP: set(),
|
ENTRY_IS_SETUP: set(),
|
||||||
"entities": {},
|
"entities": {},
|
||||||
"pending": {},
|
"pending": {},
|
||||||
|
"listener": entry.add_update_listener(update_listener),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_update_discovery_interval(
|
||||||
|
hass, entry.options.get(CONF_DISCOVERY_INTERVAL, DEFAULT_DISCOVERY_INTERVAL)
|
||||||
|
)
|
||||||
|
|
||||||
|
_update_query_interval(
|
||||||
|
hass, entry.options.get(CONF_QUERY_INTERVAL, DEFAULT_QUERY_INTERVAL)
|
||||||
|
)
|
||||||
|
|
||||||
async def async_load_devices(device_list):
|
async def async_load_devices(device_list):
|
||||||
"""Load new devices by device_list."""
|
"""Load new devices by device_list."""
|
||||||
device_type_list = {}
|
device_type_list = {}
|
||||||
|
@ -139,11 +188,13 @@ async def async_setup_entry(hass, entry):
|
||||||
else:
|
else:
|
||||||
async_dispatcher_send(hass, TUYA_DISCOVERY_NEW.format(ha_type), dev_ids)
|
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(tuya.get_all_devices())
|
||||||
await async_load_devices(device_list)
|
|
||||||
|
|
||||||
def _get_updated_devices():
|
def _get_updated_devices():
|
||||||
tuya.poll_devices_update()
|
try:
|
||||||
|
tuya.poll_devices_update()
|
||||||
|
except TuyaFrequentlyInvokeException as exc:
|
||||||
|
_LOGGER.error(exc)
|
||||||
return tuya.get_all_devices()
|
return tuya.get_all_devices()
|
||||||
|
|
||||||
async def async_poll_devices_update(event_time):
|
async def async_poll_devices_update(event_time):
|
||||||
|
@ -162,7 +213,7 @@ async def async_setup_entry(hass, entry):
|
||||||
hass.data[DOMAIN]["entities"].pop(dev_id)
|
hass.data[DOMAIN]["entities"].pop(dev_id)
|
||||||
|
|
||||||
hass.data[DOMAIN][TUYA_TRACKER] = async_track_time_interval(
|
hass.data[DOMAIN][TUYA_TRACKER] = async_track_time_interval(
|
||||||
hass, async_poll_devices_update, timedelta(minutes=5)
|
hass, async_poll_devices_update, timedelta(minutes=2)
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
|
@ -178,7 +229,7 @@ async def async_setup_entry(hass, entry):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass, entry):
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
"""Unloading the Tuya platforms."""
|
"""Unloading the Tuya platforms."""
|
||||||
unload_ok = all(
|
unload_ok = all(
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
|
@ -191,10 +242,8 @@ async def async_unload_entry(hass, entry):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if unload_ok:
|
if unload_ok:
|
||||||
hass.data[DOMAIN][ENTRY_IS_SETUP] = set()
|
hass.data[DOMAIN]["listener"]()
|
||||||
hass.data[DOMAIN][TUYA_TRACKER]()
|
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_FORCE_UPDATE)
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_PULL_DEVICES)
|
hass.services.async_remove(DOMAIN, SERVICE_PULL_DEVICES)
|
||||||
hass.data.pop(DOMAIN)
|
hass.data.pop(DOMAIN)
|
||||||
|
@ -202,20 +251,86 @@ async def async_unload_entry(hass, entry):
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Update when config_entry options update."""
|
||||||
|
hass.data[DOMAIN][TUYA_DEVICES_CONF] = entry.options.copy()
|
||||||
|
_update_discovery_interval(
|
||||||
|
hass, entry.options.get(CONF_DISCOVERY_INTERVAL, DEFAULT_DISCOVERY_INTERVAL)
|
||||||
|
)
|
||||||
|
_update_query_interval(
|
||||||
|
hass, entry.options.get(CONF_QUERY_INTERVAL, DEFAULT_QUERY_INTERVAL)
|
||||||
|
)
|
||||||
|
async_dispatcher_send(hass, SIGNAL_CONFIG_ENTITY)
|
||||||
|
|
||||||
|
|
||||||
|
async def cleanup_device_registry(hass: HomeAssistant, device_id):
|
||||||
|
"""Remove device registry entry if there are no remaining entities."""
|
||||||
|
|
||||||
|
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||||
|
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
||||||
|
if device_id and not hass.helpers.entity_registry.async_entries_for_device(
|
||||||
|
entity_registry, device_id
|
||||||
|
):
|
||||||
|
device_registry.async_remove_device(device_id)
|
||||||
|
|
||||||
|
|
||||||
class TuyaDevice(Entity):
|
class TuyaDevice(Entity):
|
||||||
"""Tuya base device."""
|
"""Tuya base device."""
|
||||||
|
|
||||||
|
_dev_can_query_count = 0
|
||||||
|
|
||||||
def __init__(self, tuya, platform):
|
def __init__(self, tuya, platform):
|
||||||
"""Init Tuya devices."""
|
"""Init Tuya devices."""
|
||||||
self._tuya = tuya
|
self._tuya = tuya
|
||||||
self._tuya_platform = platform
|
self._tuya_platform = platform
|
||||||
|
|
||||||
|
def _device_can_query(self):
|
||||||
|
"""Check if device can also use query method."""
|
||||||
|
dev_type = self._tuya.device_type()
|
||||||
|
return dev_type not in TUYA_TYPE_NOT_QUERY
|
||||||
|
|
||||||
|
def _inc_device_count(self):
|
||||||
|
"""Increment static variable device count."""
|
||||||
|
if not self._device_can_query():
|
||||||
|
return
|
||||||
|
TuyaDevice._dev_can_query_count += 1
|
||||||
|
|
||||||
|
def _dec_device_count(self):
|
||||||
|
"""Decrement static variable device count."""
|
||||||
|
if not self._device_can_query():
|
||||||
|
return
|
||||||
|
TuyaDevice._dev_can_query_count -= 1
|
||||||
|
|
||||||
|
def _get_device_config(self):
|
||||||
|
"""Get updated device options."""
|
||||||
|
devices_config = self.hass.data[DOMAIN].get(TUYA_DEVICES_CONF)
|
||||||
|
if not devices_config:
|
||||||
|
return {}
|
||||||
|
dev_conf = devices_config.get(self.object_id, {})
|
||||||
|
if dev_conf:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Configuration for deviceID %s: %s", self.object_id, str(dev_conf)
|
||||||
|
)
|
||||||
|
return dev_conf
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Call when entity is added to hass."""
|
"""Call when entity is added to hass."""
|
||||||
dev_id = self._tuya.object_id()
|
self.hass.data[DOMAIN]["entities"][self.object_id] = self.entity_id
|
||||||
self.hass.data[DOMAIN]["entities"][dev_id] = self.entity_id
|
self.async_on_remove(
|
||||||
async_dispatcher_connect(self.hass, SIGNAL_DELETE_ENTITY, self._delete_callback)
|
async_dispatcher_connect(
|
||||||
async_dispatcher_connect(self.hass, SIGNAL_UPDATE_ENTITY, self._update_callback)
|
self.hass, SIGNAL_DELETE_ENTITY, self._delete_callback
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.async_on_remove(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass, SIGNAL_UPDATE_ENTITY, self._update_callback
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._inc_device_count()
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self):
|
||||||
|
"""Call when entity is removed from hass."""
|
||||||
|
self._dec_device_count()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def object_id(self):
|
def object_id(self):
|
||||||
|
@ -252,7 +367,14 @@ class TuyaDevice(Entity):
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Refresh Tuya device data."""
|
"""Refresh Tuya device data."""
|
||||||
self._tuya.update()
|
query_dev = self.hass.data[DOMAIN][TUYA_DEVICES_CONF].get(CONF_QUERY_DEVICE, "")
|
||||||
|
use_discovery = (
|
||||||
|
TuyaDevice._dev_can_query_count > 1 and self.object_id != query_dev
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self._tuya.update(use_discovery=use_discovery)
|
||||||
|
except TuyaFrequentlyInvokeException as exc:
|
||||||
|
_LOGGER.error(exc)
|
||||||
|
|
||||||
async def _delete_callback(self, dev_id):
|
async def _delete_callback(self, dev_id):
|
||||||
"""Remove this entity."""
|
"""Remove this entity."""
|
||||||
|
@ -261,7 +383,9 @@ class TuyaDevice(Entity):
|
||||||
await self.hass.helpers.entity_registry.async_get_registry()
|
await self.hass.helpers.entity_registry.async_get_registry()
|
||||||
)
|
)
|
||||||
if entity_registry.async_is_registered(self.entity_id):
|
if entity_registry.async_is_registered(self.entity_id):
|
||||||
|
entity_entry = entity_registry.async_get(self.entity_id)
|
||||||
entity_registry.async_remove(self.entity_id)
|
entity_registry.async_remove(self.entity_id)
|
||||||
|
await cleanup_device_registry(self.hass, entity_entry.device_id)
|
||||||
else:
|
else:
|
||||||
await self.async_remove()
|
await self.async_remove()
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
"""Support for the Tuya climate devices."""
|
"""Support for the Tuya climate devices."""
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
DOMAIN as SENSOR_DOMAIN,
|
DOMAIN as SENSOR_DOMAIN,
|
||||||
ENTITY_ID_FORMAT,
|
ENTITY_ID_FORMAT,
|
||||||
|
@ -19,18 +22,32 @@ from homeassistant.components.climate.const import (
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_TEMPERATURE,
|
ATTR_TEMPERATURE,
|
||||||
CONF_PLATFORM,
|
CONF_PLATFORM,
|
||||||
|
CONF_UNIT_OF_MEASUREMENT,
|
||||||
|
ENTITY_MATCH_NONE,
|
||||||
|
PRECISION_TENTHS,
|
||||||
PRECISION_WHOLE,
|
PRECISION_WHOLE,
|
||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
TEMP_FAHRENHEIT,
|
TEMP_FAHRENHEIT,
|
||||||
)
|
)
|
||||||
|
from homeassistant.core import callback, valid_entity_id
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
from . import TuyaDevice
|
from . import TuyaDevice
|
||||||
from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
|
from .const import (
|
||||||
|
CONF_CURR_TEMP_DIVIDER,
|
||||||
|
CONF_EXT_TEMP_SENSOR,
|
||||||
|
CONF_MAX_TEMP,
|
||||||
|
CONF_MIN_TEMP,
|
||||||
|
CONF_TEMP_DIVIDER,
|
||||||
|
DOMAIN,
|
||||||
|
SIGNAL_CONFIG_ENTITY,
|
||||||
|
TUYA_DATA,
|
||||||
|
TUYA_DISCOVERY_NEW,
|
||||||
|
)
|
||||||
|
|
||||||
DEVICE_TYPE = "climate"
|
DEVICE_TYPE = "climate"
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
SCAN_INTERVAL = timedelta(seconds=15)
|
||||||
|
|
||||||
HA_STATE_TO_TUYA = {
|
HA_STATE_TO_TUYA = {
|
||||||
HVAC_MODE_AUTO: "auto",
|
HVAC_MODE_AUTO: "auto",
|
||||||
|
@ -43,6 +60,8 @@ TUYA_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_TUYA.items()}
|
||||||
|
|
||||||
FAN_MODES = {FAN_LOW, FAN_MEDIUM, FAN_HIGH}
|
FAN_MODES = {FAN_LOW, FAN_MEDIUM, FAN_HIGH}
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up tuya sensors dynamically through tuya discovery."""
|
"""Set up tuya sensors dynamically through tuya discovery."""
|
||||||
|
@ -89,21 +108,62 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
|
||||||
super().__init__(tuya, platform)
|
super().__init__(tuya, platform)
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
|
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
|
||||||
self.operations = [HVAC_MODE_OFF]
|
self.operations = [HVAC_MODE_OFF]
|
||||||
|
self._has_operation = False
|
||||||
|
self._def_hvac_mode = HVAC_MODE_AUTO
|
||||||
|
self._min_temp = None
|
||||||
|
self._max_temp = None
|
||||||
|
self._temp_entity = None
|
||||||
|
self._temp_entity_error = False
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _process_config(self):
|
||||||
|
"""Set device config parameter."""
|
||||||
|
config = self._get_device_config()
|
||||||
|
if not config:
|
||||||
|
return
|
||||||
|
unit = config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||||
|
if unit:
|
||||||
|
self._tuya.set_unit("FAHRENHEIT" if unit == TEMP_FAHRENHEIT else "CELSIUS")
|
||||||
|
self._tuya.temp_divider = config.get(CONF_TEMP_DIVIDER, 0)
|
||||||
|
self._tuya.curr_temp_divider = config.get(CONF_CURR_TEMP_DIVIDER, 0)
|
||||||
|
min_temp = config.get(CONF_MIN_TEMP, 0)
|
||||||
|
max_temp = config.get(CONF_MAX_TEMP, 0)
|
||||||
|
if min_temp >= max_temp:
|
||||||
|
self._min_temp = self._max_temp = None
|
||||||
|
else:
|
||||||
|
self._min_temp = min_temp
|
||||||
|
self._max_temp = max_temp
|
||||||
|
self._temp_entity = config.get(CONF_EXT_TEMP_SENSOR)
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Create operation list when add to hass."""
|
"""Create operation list when add to hass."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
|
self._process_config()
|
||||||
|
self.async_on_remove(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass, SIGNAL_CONFIG_ENTITY, self._process_config
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
modes = self._tuya.operation_list()
|
modes = self._tuya.operation_list()
|
||||||
if modes is None:
|
if modes is None:
|
||||||
|
if self._def_hvac_mode not in self.operations:
|
||||||
|
self.operations.append(self._def_hvac_mode)
|
||||||
return
|
return
|
||||||
|
|
||||||
for mode in modes:
|
for mode in modes:
|
||||||
if mode in TUYA_STATE_TO_HA:
|
if mode not in TUYA_STATE_TO_HA:
|
||||||
self.operations.append(TUYA_STATE_TO_HA[mode])
|
continue
|
||||||
|
ha_mode = TUYA_STATE_TO_HA[mode]
|
||||||
|
if ha_mode not in self.operations:
|
||||||
|
self.operations.append(ha_mode)
|
||||||
|
self._has_operation = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def precision(self):
|
def precision(self):
|
||||||
"""Return the precision of the system."""
|
"""Return the precision of the system."""
|
||||||
|
if self._tuya.has_decimal():
|
||||||
|
return PRECISION_TENTHS
|
||||||
return PRECISION_WHOLE
|
return PRECISION_WHOLE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -120,6 +180,9 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
|
||||||
if not self._tuya.state():
|
if not self._tuya.state():
|
||||||
return HVAC_MODE_OFF
|
return HVAC_MODE_OFF
|
||||||
|
|
||||||
|
if not self._has_operation:
|
||||||
|
return self._def_hvac_mode
|
||||||
|
|
||||||
mode = self._tuya.current_operation()
|
mode = self._tuya.current_operation()
|
||||||
if mode is None:
|
if mode is None:
|
||||||
return None
|
return None
|
||||||
|
@ -133,7 +196,10 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
|
||||||
@property
|
@property
|
||||||
def current_temperature(self):
|
def current_temperature(self):
|
||||||
"""Return the current temperature."""
|
"""Return the current temperature."""
|
||||||
return self._tuya.current_temperature()
|
curr_temp = self._tuya.current_temperature()
|
||||||
|
if curr_temp is None:
|
||||||
|
return self._get_ext_temperature()
|
||||||
|
return curr_temp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature(self):
|
def target_temperature(self):
|
||||||
|
@ -168,11 +234,13 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
|
||||||
"""Set new target operation mode."""
|
"""Set new target operation mode."""
|
||||||
if hvac_mode == HVAC_MODE_OFF:
|
if hvac_mode == HVAC_MODE_OFF:
|
||||||
self._tuya.turn_off()
|
self._tuya.turn_off()
|
||||||
|
return
|
||||||
|
|
||||||
if not self._tuya.state():
|
if not self._tuya.state():
|
||||||
self._tuya.turn_on()
|
self._tuya.turn_on()
|
||||||
|
|
||||||
self._tuya.set_operation_mode(HA_STATE_TO_TUYA.get(hvac_mode))
|
if self._has_operation:
|
||||||
|
self._tuya.set_operation_mode(HA_STATE_TO_TUYA.get(hvac_mode))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
|
@ -187,9 +255,55 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
|
||||||
@property
|
@property
|
||||||
def min_temp(self):
|
def min_temp(self):
|
||||||
"""Return the minimum temperature."""
|
"""Return the minimum temperature."""
|
||||||
return self._tuya.min_temp()
|
min_temp = (
|
||||||
|
self._min_temp if self._min_temp is not None else self._tuya.min_temp()
|
||||||
|
)
|
||||||
|
if min_temp is not None:
|
||||||
|
return min_temp
|
||||||
|
return super().min_temp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_temp(self):
|
def max_temp(self):
|
||||||
"""Return the maximum temperature."""
|
"""Return the maximum temperature."""
|
||||||
return self._tuya.max_temp()
|
max_temp = (
|
||||||
|
self._max_temp if self._max_temp is not None else self._tuya.max_temp()
|
||||||
|
)
|
||||||
|
if max_temp is not None:
|
||||||
|
return max_temp
|
||||||
|
return super().max_temp
|
||||||
|
|
||||||
|
def _set_and_log_temp_error(self, error_msg):
|
||||||
|
if not self._temp_entity_error:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Error on Tuya external temperature sensor %s: %s",
|
||||||
|
self._temp_entity,
|
||||||
|
error_msg,
|
||||||
|
)
|
||||||
|
self._temp_entity_error = True
|
||||||
|
|
||||||
|
def _get_ext_temperature(self):
|
||||||
|
"""Get external temperature entity current state."""
|
||||||
|
if not self._temp_entity or self._temp_entity == ENTITY_MATCH_NONE:
|
||||||
|
return None
|
||||||
|
|
||||||
|
entity_name = self._temp_entity
|
||||||
|
if not valid_entity_id(entity_name):
|
||||||
|
self._set_and_log_temp_error("entity name is invalid")
|
||||||
|
return None
|
||||||
|
|
||||||
|
state_obj = self.hass.states.get(entity_name)
|
||||||
|
if state_obj:
|
||||||
|
temperature = state_obj.state
|
||||||
|
try:
|
||||||
|
float(temperature)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
self._set_and_log_temp_error(
|
||||||
|
"entity state is not available or is not a number"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
self._temp_entity_error = False
|
||||||
|
return temperature
|
||||||
|
|
||||||
|
self._set_and_log_temp_error("entity not found")
|
||||||
|
return None
|
||||||
|
|
|
@ -6,13 +6,47 @@ from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException, TuyaServerExcepti
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME
|
from homeassistant.const import (
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PLATFORM,
|
||||||
|
CONF_UNIT_OF_MEASUREMENT,
|
||||||
|
CONF_USERNAME,
|
||||||
|
ENTITY_MATCH_NONE,
|
||||||
|
TEMP_CELSIUS,
|
||||||
|
TEMP_FAHRENHEIT,
|
||||||
|
)
|
||||||
|
from homeassistant.core import callback
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
# pylint:disable=unused-import
|
# pylint:disable=unused-import
|
||||||
from .const import CONF_COUNTRYCODE, DOMAIN, TUYA_PLATFORMS
|
from .const import (
|
||||||
|
CONF_BRIGHTNESS_RANGE_MODE,
|
||||||
|
CONF_COUNTRYCODE,
|
||||||
|
CONF_CURR_TEMP_DIVIDER,
|
||||||
|
CONF_DISCOVERY_INTERVAL,
|
||||||
|
CONF_EXT_TEMP_SENSOR,
|
||||||
|
CONF_MAX_KELVIN,
|
||||||
|
CONF_MAX_TEMP,
|
||||||
|
CONF_MIN_KELVIN,
|
||||||
|
CONF_MIN_TEMP,
|
||||||
|
CONF_QUERY_DEVICE,
|
||||||
|
CONF_QUERY_INTERVAL,
|
||||||
|
CONF_SUPPORT_COLOR,
|
||||||
|
CONF_TEMP_DIVIDER,
|
||||||
|
CONF_TUYA_MAX_COLTEMP,
|
||||||
|
DEFAULT_DISCOVERY_INTERVAL,
|
||||||
|
DEFAULT_QUERY_INTERVAL,
|
||||||
|
DEFAULT_TUYA_MAX_COLTEMP,
|
||||||
|
DOMAIN,
|
||||||
|
TUYA_DATA,
|
||||||
|
TUYA_PLATFORMS,
|
||||||
|
TUYA_TYPE_NOT_QUERY,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_LIST_DEVICES = "list_devices"
|
||||||
|
|
||||||
DATA_SCHEMA_USER = vol.Schema(
|
DATA_SCHEMA_USER = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_USERNAME): str,
|
vol.Required(CONF_USERNAME): str,
|
||||||
|
@ -22,6 +56,10 @@ DATA_SCHEMA_USER = vol.Schema(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ERROR_DEV_MULTI_TYPE = "dev_multi_type"
|
||||||
|
ERROR_DEV_NOT_CONFIG = "dev_not_config"
|
||||||
|
ERROR_DEV_NOT_FOUND = "dev_not_found"
|
||||||
|
|
||||||
RESULT_AUTH_FAILED = "invalid_auth"
|
RESULT_AUTH_FAILED = "invalid_auth"
|
||||||
RESULT_CONN_ERROR = "cannot_connect"
|
RESULT_CONN_ERROR = "cannot_connect"
|
||||||
RESULT_SUCCESS = "success"
|
RESULT_SUCCESS = "success"
|
||||||
|
@ -31,6 +69,8 @@ RESULT_LOG_MESSAGE = {
|
||||||
RESULT_CONN_ERROR: "Connection error",
|
RESULT_CONN_ERROR: "Connection error",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TUYA_TYPE_CONFIG = ["climate", "light"]
|
||||||
|
|
||||||
|
|
||||||
class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a tuya config flow."""
|
"""Handle a tuya config flow."""
|
||||||
|
@ -46,7 +86,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
self._username = None
|
self._username = None
|
||||||
self._is_import = False
|
self._is_import = False
|
||||||
|
|
||||||
def _get_entry(self):
|
def _save_entry(self):
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=self._username,
|
title=self._username,
|
||||||
data={
|
data={
|
||||||
|
@ -93,7 +133,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
result = await self.hass.async_add_executor_job(self._try_connect)
|
result = await self.hass.async_add_executor_job(self._try_connect)
|
||||||
|
|
||||||
if result == RESULT_SUCCESS:
|
if result == RESULT_SUCCESS:
|
||||||
return self._get_entry()
|
return self._save_entry()
|
||||||
if result != RESULT_AUTH_FAILED or self._is_import:
|
if result != RESULT_AUTH_FAILED or self._is_import:
|
||||||
if self._is_import:
|
if self._is_import:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
|
@ -106,3 +146,263 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors
|
step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(config_entry):
|
||||||
|
"""Get the options flow for this handler."""
|
||||||
|
return OptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
|
||||||
|
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
|
"""Handle a option flow for Tuya."""
|
||||||
|
|
||||||
|
def __init__(self, config_entry: config_entries.ConfigEntry):
|
||||||
|
"""Initialize options flow."""
|
||||||
|
self.config_entry = config_entry
|
||||||
|
self._conf_devs_id = None
|
||||||
|
self._conf_devs_option = {}
|
||||||
|
self._form_error = None
|
||||||
|
|
||||||
|
def _get_form_error(self):
|
||||||
|
"""Set the error to be shown in the options form."""
|
||||||
|
errors = {}
|
||||||
|
if self._form_error:
|
||||||
|
errors["base"] = self._form_error
|
||||||
|
self._form_error = None
|
||||||
|
return errors
|
||||||
|
|
||||||
|
def _get_tuya_devices_filtered(self, types, exclude_mode=False, type_prefix=True):
|
||||||
|
"""Get the list of Tuya device to filtered by types."""
|
||||||
|
config_list = {}
|
||||||
|
types_filter = set(types)
|
||||||
|
tuya = self.hass.data[DOMAIN][TUYA_DATA]
|
||||||
|
devices_list = tuya.get_all_devices()
|
||||||
|
for device in devices_list:
|
||||||
|
dev_type = device.device_type()
|
||||||
|
exclude = (
|
||||||
|
dev_type in types_filter
|
||||||
|
if exclude_mode
|
||||||
|
else dev_type not in types_filter
|
||||||
|
)
|
||||||
|
if exclude:
|
||||||
|
continue
|
||||||
|
dev_id = device.object_id()
|
||||||
|
if type_prefix:
|
||||||
|
dev_id = f"{dev_type}-{dev_id}"
|
||||||
|
config_list[dev_id] = f"{device.name()} ({dev_type})"
|
||||||
|
|
||||||
|
return config_list
|
||||||
|
|
||||||
|
def _get_device(self, dev_id):
|
||||||
|
"""Get specific device from tuya library."""
|
||||||
|
tuya = self.hass.data[DOMAIN][TUYA_DATA]
|
||||||
|
return tuya.get_device_by_id(dev_id)
|
||||||
|
|
||||||
|
def _save_config(self, data):
|
||||||
|
"""Save the updated options."""
|
||||||
|
curr_conf = self.config_entry.options.copy()
|
||||||
|
curr_conf.update(data)
|
||||||
|
curr_conf.update(self._conf_devs_option)
|
||||||
|
|
||||||
|
return self.async_create_entry(title="", data=curr_conf)
|
||||||
|
|
||||||
|
async def _async_device_form(self, devs_id):
|
||||||
|
"""Return configuration form for devices."""
|
||||||
|
conf_devs_id = []
|
||||||
|
for count, dev_id in enumerate(devs_id):
|
||||||
|
device_info = dev_id.split("-")
|
||||||
|
if count == 0:
|
||||||
|
device_type = device_info[0]
|
||||||
|
device_id = device_info[1]
|
||||||
|
elif device_type != device_info[0]:
|
||||||
|
self._form_error = ERROR_DEV_MULTI_TYPE
|
||||||
|
return await self.async_step_init()
|
||||||
|
conf_devs_id.append(device_info[1])
|
||||||
|
|
||||||
|
device = self._get_device(device_id)
|
||||||
|
if not device:
|
||||||
|
self._form_error = ERROR_DEV_NOT_FOUND
|
||||||
|
return await self.async_step_init()
|
||||||
|
|
||||||
|
curr_conf = self._conf_devs_option.get(
|
||||||
|
device_id, self.config_entry.options.get(device_id, {})
|
||||||
|
)
|
||||||
|
|
||||||
|
config_schema = await self._get_device_schema(device_type, curr_conf, device)
|
||||||
|
if not config_schema:
|
||||||
|
self._form_error = ERROR_DEV_NOT_CONFIG
|
||||||
|
return await self.async_step_init()
|
||||||
|
|
||||||
|
self._conf_devs_id = conf_devs_id
|
||||||
|
device_name = (
|
||||||
|
"(multiple devices selected)" if len(conf_devs_id) > 1 else device.name()
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="device",
|
||||||
|
data_schema=config_schema,
|
||||||
|
description_placeholders={
|
||||||
|
"device_type": device_type,
|
||||||
|
"device_name": device_name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_init(self, user_input=None):
|
||||||
|
"""Handle options flow."""
|
||||||
|
if user_input is not None:
|
||||||
|
dev_ids = user_input.get(CONF_LIST_DEVICES)
|
||||||
|
if dev_ids:
|
||||||
|
return await self.async_step_device(None, dev_ids)
|
||||||
|
|
||||||
|
user_input.pop(CONF_LIST_DEVICES, [])
|
||||||
|
return self._save_config(data=user_input)
|
||||||
|
|
||||||
|
data_schema = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(
|
||||||
|
CONF_DISCOVERY_INTERVAL,
|
||||||
|
default=self.config_entry.options.get(
|
||||||
|
CONF_DISCOVERY_INTERVAL, DEFAULT_DISCOVERY_INTERVAL
|
||||||
|
),
|
||||||
|
): vol.All(vol.Coerce(int), vol.Clamp(min=30, max=900)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
query_devices = self._get_tuya_devices_filtered(
|
||||||
|
TUYA_TYPE_NOT_QUERY, True, False
|
||||||
|
)
|
||||||
|
if query_devices:
|
||||||
|
devices = {ENTITY_MATCH_NONE: "Default"}
|
||||||
|
devices.update(query_devices)
|
||||||
|
def_val = self.config_entry.options.get(CONF_QUERY_DEVICE)
|
||||||
|
if not def_val or not query_devices.get(def_val):
|
||||||
|
def_val = ENTITY_MATCH_NONE
|
||||||
|
data_schema = data_schema.extend(
|
||||||
|
{
|
||||||
|
vol.Optional(
|
||||||
|
CONF_QUERY_INTERVAL,
|
||||||
|
default=self.config_entry.options.get(
|
||||||
|
CONF_QUERY_INTERVAL, DEFAULT_QUERY_INTERVAL
|
||||||
|
),
|
||||||
|
): vol.All(vol.Coerce(int), vol.Clamp(min=30, max=240)),
|
||||||
|
vol.Optional(CONF_QUERY_DEVICE, default=def_val): vol.In(devices),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
config_devices = self._get_tuya_devices_filtered(TUYA_TYPE_CONFIG, False, True)
|
||||||
|
if config_devices:
|
||||||
|
data_schema = data_schema.extend(
|
||||||
|
{vol.Optional(CONF_LIST_DEVICES): cv.multi_select(config_devices)}
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="init",
|
||||||
|
data_schema=data_schema,
|
||||||
|
errors=self._get_form_error(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_device(self, user_input=None, dev_ids=None):
|
||||||
|
"""Handle options flow for device."""
|
||||||
|
if dev_ids is not None:
|
||||||
|
return await self._async_device_form(dev_ids)
|
||||||
|
if user_input is not None:
|
||||||
|
for device_id in self._conf_devs_id:
|
||||||
|
self._conf_devs_option[device_id] = user_input
|
||||||
|
|
||||||
|
return await self.async_step_init()
|
||||||
|
|
||||||
|
async def _get_device_schema(self, device_type, curr_conf, device):
|
||||||
|
"""Return option schema for device."""
|
||||||
|
if device_type == "light":
|
||||||
|
return self._get_light_schema(curr_conf, device)
|
||||||
|
if device_type == "climate":
|
||||||
|
entities_list = await _get_entities_matching_domains(self.hass, ["sensor"])
|
||||||
|
return self._get_climate_schema(curr_conf, device, entities_list)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_light_schema(curr_conf, device):
|
||||||
|
"""Create option schema for light device."""
|
||||||
|
min_kelvin = device.max_color_temp()
|
||||||
|
max_kelvin = device.min_color_temp()
|
||||||
|
|
||||||
|
config_schema = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(
|
||||||
|
CONF_SUPPORT_COLOR,
|
||||||
|
default=curr_conf.get(CONF_SUPPORT_COLOR, False),
|
||||||
|
): bool,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_BRIGHTNESS_RANGE_MODE,
|
||||||
|
default=curr_conf.get(CONF_BRIGHTNESS_RANGE_MODE, 0),
|
||||||
|
): vol.In({0: "Range 1-255", 1: "Range 10-1000"}),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MIN_KELVIN,
|
||||||
|
default=curr_conf.get(CONF_MIN_KELVIN, min_kelvin),
|
||||||
|
): vol.All(vol.Coerce(int), vol.Clamp(min=min_kelvin, max=max_kelvin)),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MAX_KELVIN,
|
||||||
|
default=curr_conf.get(CONF_MAX_KELVIN, max_kelvin),
|
||||||
|
): vol.All(vol.Coerce(int), vol.Clamp(min=min_kelvin, max=max_kelvin)),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_TUYA_MAX_COLTEMP,
|
||||||
|
default=curr_conf.get(
|
||||||
|
CONF_TUYA_MAX_COLTEMP, DEFAULT_TUYA_MAX_COLTEMP
|
||||||
|
),
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Clamp(
|
||||||
|
min=DEFAULT_TUYA_MAX_COLTEMP, max=DEFAULT_TUYA_MAX_COLTEMP * 10
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return config_schema
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_climate_schema(curr_conf, device, entities_list):
|
||||||
|
"""Create option schema for climate device."""
|
||||||
|
unit = device.temperature_unit()
|
||||||
|
def_unit = TEMP_FAHRENHEIT if unit == "FAHRENHEIT" else TEMP_CELSIUS
|
||||||
|
entities_list.insert(0, ENTITY_MATCH_NONE)
|
||||||
|
|
||||||
|
config_schema = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(
|
||||||
|
CONF_UNIT_OF_MEASUREMENT,
|
||||||
|
default=curr_conf.get(CONF_UNIT_OF_MEASUREMENT, def_unit),
|
||||||
|
): vol.In({TEMP_CELSIUS: "Celsius", TEMP_FAHRENHEIT: "Fahrenheit"}),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_TEMP_DIVIDER,
|
||||||
|
default=curr_conf.get(CONF_TEMP_DIVIDER, 0),
|
||||||
|
): vol.All(vol.Coerce(int), vol.Clamp(min=0)),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_CURR_TEMP_DIVIDER,
|
||||||
|
default=curr_conf.get(CONF_CURR_TEMP_DIVIDER, 0),
|
||||||
|
): vol.All(vol.Coerce(int), vol.Clamp(min=0)),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MIN_TEMP,
|
||||||
|
default=curr_conf.get(CONF_MIN_TEMP, 0),
|
||||||
|
): int,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MAX_TEMP,
|
||||||
|
default=curr_conf.get(CONF_MAX_TEMP, 0),
|
||||||
|
): int,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_EXT_TEMP_SENSOR,
|
||||||
|
default=curr_conf.get(CONF_EXT_TEMP_SENSOR, ENTITY_MATCH_NONE),
|
||||||
|
): vol.In(entities_list),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return config_schema
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_entities_matching_domains(hass, domains):
|
||||||
|
"""List entities in the given domains."""
|
||||||
|
included_domains = set(domains)
|
||||||
|
entity_ids = hass.states.async_entity_ids(included_domains)
|
||||||
|
entity_ids.sort()
|
||||||
|
return entity_ids
|
||||||
|
|
|
@ -1,10 +1,32 @@
|
||||||
"""Constants for the Tuya integration."""
|
"""Constants for the Tuya integration."""
|
||||||
|
|
||||||
|
CONF_BRIGHTNESS_RANGE_MODE = "brightness_range_mode"
|
||||||
CONF_COUNTRYCODE = "country_code"
|
CONF_COUNTRYCODE = "country_code"
|
||||||
|
CONF_CURR_TEMP_DIVIDER = "curr_temp_divider"
|
||||||
|
CONF_DISCOVERY_INTERVAL = "discovery_interval"
|
||||||
|
CONF_EXT_TEMP_SENSOR = "ext_temp_sensor"
|
||||||
|
CONF_MAX_KELVIN = "max_kelvin"
|
||||||
|
CONF_MAX_TEMP = "max_temp"
|
||||||
|
CONF_MIN_KELVIN = "min_kelvin"
|
||||||
|
CONF_MIN_TEMP = "min_temp"
|
||||||
|
CONF_QUERY_DEVICE = "query_device"
|
||||||
|
CONF_QUERY_INTERVAL = "query_interval"
|
||||||
|
CONF_SUPPORT_COLOR = "support_color"
|
||||||
|
CONF_TEMP_DIVIDER = "temp_divider"
|
||||||
|
CONF_TUYA_MAX_COLTEMP = "tuya_max_coltemp"
|
||||||
|
|
||||||
|
DEFAULT_DISCOVERY_INTERVAL = 605
|
||||||
|
DEFAULT_QUERY_INTERVAL = 120
|
||||||
|
DEFAULT_TUYA_MAX_COLTEMP = 10000
|
||||||
|
|
||||||
DOMAIN = "tuya"
|
DOMAIN = "tuya"
|
||||||
|
|
||||||
|
SIGNAL_CONFIG_ENTITY = "tuya_config"
|
||||||
|
SIGNAL_DELETE_ENTITY = "tuya_delete"
|
||||||
|
SIGNAL_UPDATE_ENTITY = "tuya_update"
|
||||||
|
|
||||||
TUYA_DATA = "tuya_data"
|
TUYA_DATA = "tuya_data"
|
||||||
|
TUYA_DEVICES_CONF = "devices_config"
|
||||||
TUYA_DISCOVERY_NEW = "tuya_discovery_new_{}"
|
TUYA_DISCOVERY_NEW = "tuya_discovery_new_{}"
|
||||||
|
|
||||||
TUYA_PLATFORMS = {
|
TUYA_PLATFORMS = {
|
||||||
|
@ -12,3 +34,5 @@ TUYA_PLATFORMS = {
|
||||||
"smart_life": "Smart Life",
|
"smart_life": "Smart Life",
|
||||||
"jinvoo_smart": "Jinvoo Smart",
|
"jinvoo_smart": "Jinvoo Smart",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TUYA_TYPE_NOT_QUERY = ["scene", "switch"]
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
"""Support for Tuya covers."""
|
"""Support for Tuya covers."""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from homeassistant.components.cover import (
|
from homeassistant.components.cover import (
|
||||||
DOMAIN as SENSOR_DOMAIN,
|
DOMAIN as SENSOR_DOMAIN,
|
||||||
ENTITY_ID_FORMAT,
|
ENTITY_ID_FORMAT,
|
||||||
|
@ -13,7 +15,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from . import TuyaDevice
|
from . import TuyaDevice
|
||||||
from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
|
from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
SCAN_INTERVAL = timedelta(seconds=15)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
@ -60,6 +62,8 @@ class TuyaCover(TuyaDevice, CoverEntity):
|
||||||
"""Init tuya cover device."""
|
"""Init tuya cover device."""
|
||||||
super().__init__(tuya, platform)
|
super().__init__(tuya, platform)
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
|
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
|
||||||
|
self._was_closing = False
|
||||||
|
self._was_opening = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
|
@ -69,14 +73,34 @@ class TuyaCover(TuyaDevice, CoverEntity):
|
||||||
supported_features |= SUPPORT_STOP
|
supported_features |= SUPPORT_STOP
|
||||||
return supported_features
|
return supported_features
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_opening(self):
|
||||||
|
"""Return if the cover is opening or not."""
|
||||||
|
state = self._tuya.state()
|
||||||
|
if state == 1:
|
||||||
|
self._was_opening = True
|
||||||
|
self._was_closing = False
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closing(self):
|
||||||
|
"""Return if the cover is closing or not."""
|
||||||
|
state = self._tuya.state()
|
||||||
|
if state == 2:
|
||||||
|
self._was_opening = False
|
||||||
|
self._was_closing = True
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_closed(self):
|
def is_closed(self):
|
||||||
"""Return if the cover is closed or not."""
|
"""Return if the cover is closed or not."""
|
||||||
state = self._tuya.state()
|
state = self._tuya.state()
|
||||||
if state == 1:
|
if state != 2 and self._was_closing:
|
||||||
return False
|
|
||||||
if state == 2:
|
|
||||||
return True
|
return True
|
||||||
|
if state != 1 and self._was_opening:
|
||||||
|
return False
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def open_cover(self, **kwargs):
|
def open_cover(self, **kwargs):
|
||||||
|
@ -89,4 +113,7 @@ class TuyaCover(TuyaDevice, CoverEntity):
|
||||||
|
|
||||||
def stop_cover(self, **kwargs):
|
def stop_cover(self, **kwargs):
|
||||||
"""Stop the cover."""
|
"""Stop the cover."""
|
||||||
|
if self.is_closed is None:
|
||||||
|
self._was_opening = False
|
||||||
|
self._was_closing = False
|
||||||
self._tuya.stop_cover()
|
self._tuya.stop_cover()
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
"""Support for Tuya fans."""
|
"""Support for Tuya fans."""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from homeassistant.components.fan import (
|
from homeassistant.components.fan import (
|
||||||
DOMAIN as SENSOR_DOMAIN,
|
DOMAIN as SENSOR_DOMAIN,
|
||||||
ENTITY_ID_FORMAT,
|
ENTITY_ID_FORMAT,
|
||||||
|
@ -12,7 +14,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from . import TuyaDevice
|
from . import TuyaDevice
|
||||||
from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
|
from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
SCAN_INTERVAL = timedelta(seconds=15)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
"""Support for the Tuya lights."""
|
"""Support for the Tuya lights."""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
ATTR_COLOR_TEMP,
|
ATTR_COLOR_TEMP,
|
||||||
|
@ -11,13 +13,33 @@ from homeassistant.components.light import (
|
||||||
LightEntity,
|
LightEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_PLATFORM
|
from homeassistant.const import CONF_PLATFORM
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.util import color as colorutil
|
from homeassistant.util import color as colorutil
|
||||||
|
|
||||||
from . import TuyaDevice
|
from . import TuyaDevice
|
||||||
from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
|
from .const import (
|
||||||
|
CONF_BRIGHTNESS_RANGE_MODE,
|
||||||
|
CONF_MAX_KELVIN,
|
||||||
|
CONF_MIN_KELVIN,
|
||||||
|
CONF_SUPPORT_COLOR,
|
||||||
|
CONF_TUYA_MAX_COLTEMP,
|
||||||
|
DEFAULT_TUYA_MAX_COLTEMP,
|
||||||
|
DOMAIN,
|
||||||
|
SIGNAL_CONFIG_ENTITY,
|
||||||
|
TUYA_DATA,
|
||||||
|
TUYA_DISCOVERY_NEW,
|
||||||
|
)
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
SCAN_INTERVAL = timedelta(seconds=15)
|
||||||
|
|
||||||
|
TUYA_BRIGHTNESS_RANGE0 = (1, 255)
|
||||||
|
TUYA_BRIGHTNESS_RANGE1 = (10, 1000)
|
||||||
|
|
||||||
|
BRIGHTNESS_MODES = {
|
||||||
|
0: TUYA_BRIGHTNESS_RANGE0,
|
||||||
|
1: TUYA_BRIGHTNESS_RANGE1,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
@ -64,6 +86,49 @@ class TuyaLight(TuyaDevice, LightEntity):
|
||||||
"""Init Tuya light device."""
|
"""Init Tuya light device."""
|
||||||
super().__init__(tuya, platform)
|
super().__init__(tuya, platform)
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
|
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
|
||||||
|
self._min_kelvin = tuya.max_color_temp()
|
||||||
|
self._max_kelvin = tuya.min_color_temp()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _process_config(self):
|
||||||
|
"""Set device config parameter."""
|
||||||
|
config = self._get_device_config()
|
||||||
|
if not config:
|
||||||
|
return
|
||||||
|
|
||||||
|
# support color config
|
||||||
|
supp_color = config.get(CONF_SUPPORT_COLOR, False)
|
||||||
|
if supp_color:
|
||||||
|
self._tuya.force_support_color()
|
||||||
|
# brightness range config
|
||||||
|
self._tuya.brightness_white_range = BRIGHTNESS_MODES.get(
|
||||||
|
config.get(CONF_BRIGHTNESS_RANGE_MODE, 0),
|
||||||
|
TUYA_BRIGHTNESS_RANGE0,
|
||||||
|
)
|
||||||
|
# color set temp range
|
||||||
|
min_tuya = self._tuya.max_color_temp()
|
||||||
|
min_kelvin = config.get(CONF_MIN_KELVIN, min_tuya)
|
||||||
|
max_tuya = self._tuya.min_color_temp()
|
||||||
|
max_kelvin = config.get(CONF_MAX_KELVIN, max_tuya)
|
||||||
|
self._min_kelvin = min(max(min_kelvin, min_tuya), max_tuya)
|
||||||
|
self._max_kelvin = min(max(max_kelvin, self._min_kelvin), max_tuya)
|
||||||
|
# color shown temp range
|
||||||
|
max_color_temp = max(
|
||||||
|
config.get(CONF_TUYA_MAX_COLTEMP, DEFAULT_TUYA_MAX_COLTEMP),
|
||||||
|
DEFAULT_TUYA_MAX_COLTEMP,
|
||||||
|
)
|
||||||
|
self._tuya.color_temp_range = (1000, max_color_temp)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Set config parameter when add to hass."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
self._process_config()
|
||||||
|
self.async_on_remove(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass, SIGNAL_CONFIG_ENTITY, self._process_config
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self):
|
def brightness(self):
|
||||||
|
@ -93,12 +158,12 @@ class TuyaLight(TuyaDevice, LightEntity):
|
||||||
@property
|
@property
|
||||||
def min_mireds(self):
|
def min_mireds(self):
|
||||||
"""Return color temperature min mireds."""
|
"""Return color temperature min mireds."""
|
||||||
return colorutil.color_temperature_kelvin_to_mired(self._tuya.min_color_temp())
|
return colorutil.color_temperature_kelvin_to_mired(self._max_kelvin)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_mireds(self):
|
def max_mireds(self):
|
||||||
"""Return color temperature max mireds."""
|
"""Return color temperature max mireds."""
|
||||||
return colorutil.color_temperature_kelvin_to_mired(self._tuya.max_color_temp())
|
return colorutil.color_temperature_kelvin_to_mired(self._min_kelvin)
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
"""Turn on or control the light."""
|
"""Turn on or control the light."""
|
||||||
|
|
|
@ -10,8 +10,6 @@ from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
|
||||||
|
|
||||||
ENTITY_ID_FORMAT = SENSOR_DOMAIN + ".{}"
|
ENTITY_ID_FORMAT = SENSOR_DOMAIN + ".{}"
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up tuya sensors dynamically through tuya discovery."""
|
"""Set up tuya sensors dynamically through tuya discovery."""
|
||||||
|
|
|
@ -21,5 +21,41 @@
|
||||||
"error": {
|
"error": {
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Configure Tuya Options",
|
||||||
|
"description": "Do not set pollings interval values too low or the calls will fail generating error message in the log",
|
||||||
|
"data": {
|
||||||
|
"discovery_interval": "Discovery device polling interval in seconds",
|
||||||
|
"query_device": "Select device that will use query method for faster status update",
|
||||||
|
"query_interval": "Query device polling interval in seconds",
|
||||||
|
"list_devices": "Select the devices to configure or leave empty to save configuration"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"device": {
|
||||||
|
"title": "Configure Tuya Device",
|
||||||
|
"description": "Configure options to adjust displayed information for {device_type} device `{device_name}`",
|
||||||
|
"data": {
|
||||||
|
"support_color": "Force color support",
|
||||||
|
"brightness_range_mode": "Brightness range used by device",
|
||||||
|
"min_kelvin": "Min color temperature supported in kelvin",
|
||||||
|
"max_kelvin": "Max color temperature supported in kelvin",
|
||||||
|
"tuya_max_coltemp": "Max color temperature reported by device",
|
||||||
|
"unit_of_measurement": "Temperature unit used by device",
|
||||||
|
"temp_divider": "Temperature values divider (0 = use default)",
|
||||||
|
"curr_temp_divider": "Current Temperature value divider (0 = use default)",
|
||||||
|
"min_temp": "Min target temperature (use min and max = 0 for default)",
|
||||||
|
"max_temp": "Max target temperature (use min and max = 0 for default)",
|
||||||
|
"ext_temp_sensor": "Sensor for current temperature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"dev_multi_type": "Multiple selected devices to configure must be of the same type",
|
||||||
|
"dev_not_config": "Device type not configurable",
|
||||||
|
"dev_not_found": "Device not found"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
"""Support for Tuya switches."""
|
"""Support for Tuya switches."""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from homeassistant.components.switch import (
|
from homeassistant.components.switch import (
|
||||||
DOMAIN as SENSOR_DOMAIN,
|
DOMAIN as SENSOR_DOMAIN,
|
||||||
ENTITY_ID_FORMAT,
|
ENTITY_ID_FORMAT,
|
||||||
|
@ -10,7 +12,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from . import TuyaDevice
|
from . import TuyaDevice
|
||||||
from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
|
from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
SCAN_INTERVAL = timedelta(seconds=15)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"auth_failed": "Invalid authentication",
|
|
||||||
"cannot_connect": "Failed to connect",
|
"cannot_connect": "Failed to connect",
|
||||||
"conn_error": "Failed to connect",
|
|
||||||
"invalid_auth": "Invalid authentication",
|
"invalid_auth": "Invalid authentication",
|
||||||
"single_instance_allowed": "Already configured. Only a single configuration possible."
|
"single_instance_allowed": "Already configured. Only a single configuration possible."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"auth_failed": "Invalid authentication",
|
|
||||||
"invalid_auth": "Invalid authentication"
|
"invalid_auth": "Invalid authentication"
|
||||||
},
|
},
|
||||||
"flow_title": "Tuya configuration",
|
"flow_title": "Tuya configuration",
|
||||||
|
@ -24,5 +21,41 @@
|
||||||
"title": "Tuya"
|
"title": "Tuya"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"error": {
|
||||||
|
"dev_multi_type": "Multiple selected devices to configure must be of the same type",
|
||||||
|
"dev_not_config": "Device type not configurable",
|
||||||
|
"dev_not_found": "Device not found"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Configure Tuya Options",
|
||||||
|
"description": "Do not set pollings interval values too low or the calls will fail generating error message in the log",
|
||||||
|
"data": {
|
||||||
|
"discovery_interval": "Discovery device polling interval in seconds",
|
||||||
|
"query_device": "Select device that will use query method for faster status update",
|
||||||
|
"query_interval": "Query device polling interval in seconds",
|
||||||
|
"list_devices": "Select the devices to configure or leave empty to save configuration"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"device": {
|
||||||
|
"title": "Configure Tuya Device",
|
||||||
|
"description": "Configure options to adjust displayed information for {device_type} device `{device_name}`",
|
||||||
|
"data": {
|
||||||
|
"support_color": "Force color support",
|
||||||
|
"brightness_range_mode": "Brightness range used by device",
|
||||||
|
"min_kelvin": "Min color temperature supported in kelvin",
|
||||||
|
"max_kelvin": "Max color temperature supported in kelvin",
|
||||||
|
"tuya_max_coltemp": "Max color temperature reported by device",
|
||||||
|
"unit_of_measurement": "Temperature unit used by device",
|
||||||
|
"temp_divider": "Temperature values divider (0 = use default)",
|
||||||
|
"curr_temp_divider": "Current Temperature value divider (0 = use default)",
|
||||||
|
"min_temp": "Min target temperature (use min and max = 0 for default)",
|
||||||
|
"max_temp": "Max target temperature (use min and max = 0 for default)",
|
||||||
|
"ext_temp_sensor": "Sensor for current temperature"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue