Update tuyaha to 0.0.8 and adapt code (#41375)

This commit is contained in:
ollo69 2020-10-28 02:26:18 +01:00 committed by GitHub
parent 2858a90d5c
commit c4b3cf0788
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 776 additions and 51 deletions

View file

@ -4,12 +4,17 @@ from datetime import timedelta
import logging
from tuyaha import TuyaApi
from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException, TuyaServerException
from tuyaha.tuyaapi import (
TuyaAPIException,
TuyaFrequentlyInvokeException,
TuyaNetException,
TuyaServerException,
)
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.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
@ -21,24 +26,30 @@ from homeassistant.helpers.event import async_track_time_interval
from .const import (
CONF_COUNTRYCODE,
CONF_DISCOVERY_INTERVAL,
CONF_QUERY_DEVICE,
CONF_QUERY_INTERVAL,
DEFAULT_DISCOVERY_INTERVAL,
DEFAULT_QUERY_INTERVAL,
DOMAIN,
SIGNAL_CONFIG_ENTITY,
SIGNAL_DELETE_ENTITY,
SIGNAL_UPDATE_ENTITY,
TUYA_DATA,
TUYA_DEVICES_CONF,
TUYA_DISCOVERY_NEW,
TUYA_PLATFORMS,
TUYA_TYPE_NOT_QUERY,
)
_LOGGER = logging.getLogger(__name__)
ATTR_TUYA_DEV_ID = "tuya_device_id"
ENTRY_IS_SETUP = "tuya_entry_is_setup"
PARALLEL_UPDATES = 0
SERVICE_FORCE_UPDATE = "force_update"
SERVICE_PULL_DEVICES = "pull_devices"
SIGNAL_DELETE_ENTITY = "tuya_delete"
SIGNAL_UPDATE_ENTITY = "tuya_update"
TUYA_TYPE_TO_HA = {
"climate": "climate",
"cover": "cover",
@ -56,9 +67,9 @@ 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.Required(CONF_PASSWORD): 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):
"""Set up the Tuya integration."""
@ -82,7 +117,7 @@ async def async_setup(hass, config):
return True
async def async_setup_entry(hass, entry):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Tuya platform."""
tuya = TuyaApi()
@ -95,7 +130,11 @@ async def async_setup_entry(hass, entry):
await hass.async_add_executor_job(
tuya.init, username, password, country_code, platform
)
except (TuyaNetException, TuyaServerException) as exc:
except (
TuyaNetException,
TuyaServerException,
TuyaFrequentlyInvokeException,
) as exc:
raise ConfigEntryNotReady() from exc
except TuyaAPIException as exc:
@ -107,12 +146,22 @@ async def async_setup_entry(hass, entry):
hass.data[DOMAIN] = {
TUYA_DATA: tuya,
TUYA_DEVICES_CONF: entry.options.copy(),
TUYA_TRACKER: None,
ENTRY_IS_SETUP: set(),
"entities": {},
"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):
"""Load new devices by device_list."""
device_type_list = {}
@ -139,11 +188,13 @@ async def async_setup_entry(hass, entry):
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)
await async_load_devices(tuya.get_all_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()
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][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(
@ -178,7 +229,7 @@ async def async_setup_entry(hass, entry):
return True
async def async_unload_entry(hass, entry):
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unloading the Tuya platforms."""
unload_ok = all(
await asyncio.gather(
@ -191,10 +242,8 @@ async def async_unload_entry(hass, entry):
)
)
if unload_ok:
hass.data[DOMAIN][ENTRY_IS_SETUP] = set()
hass.data[DOMAIN]["listener"]()
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)
@ -202,20 +251,86 @@ async def async_unload_entry(hass, entry):
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):
"""Tuya base device."""
_dev_can_query_count = 0
def __init__(self, tuya, platform):
"""Init Tuya devices."""
self._tuya = tuya
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):
"""Call when entity is added to hass."""
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)
self.hass.data[DOMAIN]["entities"][self.object_id] = self.entity_id
self.async_on_remove(
async_dispatcher_connect(
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
def object_id(self):
@ -252,7 +367,14 @@ class TuyaDevice(Entity):
def update(self):
"""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):
"""Remove this entity."""
@ -261,7 +383,9 @@ class TuyaDevice(Entity):
await self.hass.helpers.entity_registry.async_get_registry()
)
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)
await cleanup_device_registry(self.hass, entity_entry.device_id)
else:
await self.async_remove()

View file

@ -1,4 +1,7 @@
"""Support for the Tuya climate devices."""
from datetime import timedelta
import logging
from homeassistant.components.climate import (
DOMAIN as SENSOR_DOMAIN,
ENTITY_ID_FORMAT,
@ -19,18 +22,32 @@ from homeassistant.components.climate.const import (
from homeassistant.const import (
ATTR_TEMPERATURE,
CONF_PLATFORM,
CONF_UNIT_OF_MEASUREMENT,
ENTITY_MATCH_NONE,
PRECISION_TENTHS,
PRECISION_WHOLE,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
from homeassistant.core import callback, valid_entity_id
from homeassistant.helpers.dispatcher import async_dispatcher_connect
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"
PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(seconds=15)
HA_STATE_TO_TUYA = {
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}
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up tuya sensors dynamically through tuya discovery."""
@ -89,21 +108,62 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
super().__init__(tuya, platform)
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
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):
"""Create operation list 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
)
)
modes = self._tuya.operation_list()
if modes is None:
if self._def_hvac_mode not in self.operations:
self.operations.append(self._def_hvac_mode)
return
for mode in modes:
if mode in TUYA_STATE_TO_HA:
self.operations.append(TUYA_STATE_TO_HA[mode])
if mode not in TUYA_STATE_TO_HA:
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
def precision(self):
"""Return the precision of the system."""
if self._tuya.has_decimal():
return PRECISION_TENTHS
return PRECISION_WHOLE
@property
@ -120,6 +180,9 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
if not self._tuya.state():
return HVAC_MODE_OFF
if not self._has_operation:
return self._def_hvac_mode
mode = self._tuya.current_operation()
if mode is None:
return None
@ -133,7 +196,10 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
@property
def current_temperature(self):
"""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
def target_temperature(self):
@ -168,11 +234,13 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
"""Set new target operation mode."""
if hvac_mode == HVAC_MODE_OFF:
self._tuya.turn_off()
return
if not self._tuya.state():
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
def supported_features(self):
@ -187,9 +255,55 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
@property
def min_temp(self):
"""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
def max_temp(self):
"""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

View file

@ -6,13 +6,47 @@ from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException, TuyaServerExcepti
import voluptuous as vol
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
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__)
CONF_LIST_DEVICES = "list_devices"
DATA_SCHEMA_USER = vol.Schema(
{
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_CONN_ERROR = "cannot_connect"
RESULT_SUCCESS = "success"
@ -31,6 +69,8 @@ RESULT_LOG_MESSAGE = {
RESULT_CONN_ERROR: "Connection error",
}
TUYA_TYPE_CONFIG = ["climate", "light"]
class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a tuya config flow."""
@ -46,7 +86,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._username = None
self._is_import = False
def _get_entry(self):
def _save_entry(self):
return self.async_create_entry(
title=self._username,
data={
@ -93,7 +133,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
result = await self.hass.async_add_executor_job(self._try_connect)
if result == RESULT_SUCCESS:
return self._get_entry()
return self._save_entry()
if result != RESULT_AUTH_FAILED or self._is_import:
if self._is_import:
_LOGGER.error(
@ -106,3 +146,263 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(
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

View file

@ -1,10 +1,32 @@
"""Constants for the Tuya integration."""
CONF_BRIGHTNESS_RANGE_MODE = "brightness_range_mode"
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"
SIGNAL_CONFIG_ENTITY = "tuya_config"
SIGNAL_DELETE_ENTITY = "tuya_delete"
SIGNAL_UPDATE_ENTITY = "tuya_update"
TUYA_DATA = "tuya_data"
TUYA_DEVICES_CONF = "devices_config"
TUYA_DISCOVERY_NEW = "tuya_discovery_new_{}"
TUYA_PLATFORMS = {
@ -12,3 +34,5 @@ TUYA_PLATFORMS = {
"smart_life": "Smart Life",
"jinvoo_smart": "Jinvoo Smart",
}
TUYA_TYPE_NOT_QUERY = ["scene", "switch"]

View file

@ -1,4 +1,6 @@
"""Support for Tuya covers."""
from datetime import timedelta
from homeassistant.components.cover import (
DOMAIN as SENSOR_DOMAIN,
ENTITY_ID_FORMAT,
@ -13,7 +15,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import TuyaDevice
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):
@ -60,6 +62,8 @@ class TuyaCover(TuyaDevice, CoverEntity):
"""Init tuya cover device."""
super().__init__(tuya, platform)
self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id())
self._was_closing = False
self._was_opening = False
@property
def supported_features(self):
@ -69,14 +73,34 @@ class TuyaCover(TuyaDevice, CoverEntity):
supported_features |= SUPPORT_STOP
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
def is_closed(self):
"""Return if the cover is closed or not."""
state = self._tuya.state()
if state == 1:
return False
if state == 2:
if state != 2 and self._was_closing:
return True
if state != 1 and self._was_opening:
return False
return None
def open_cover(self, **kwargs):
@ -89,4 +113,7 @@ class TuyaCover(TuyaDevice, CoverEntity):
def stop_cover(self, **kwargs):
"""Stop the cover."""
if self.is_closed is None:
self._was_opening = False
self._was_closing = False
self._tuya.stop_cover()

View file

@ -1,4 +1,6 @@
"""Support for Tuya fans."""
from datetime import timedelta
from homeassistant.components.fan import (
DOMAIN as SENSOR_DOMAIN,
ENTITY_ID_FORMAT,
@ -12,7 +14,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import TuyaDevice
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):

View file

@ -1,4 +1,6 @@
"""Support for the Tuya lights."""
from datetime import timedelta
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
@ -11,13 +13,33 @@ from homeassistant.components.light import (
LightEntity,
)
from homeassistant.const import CONF_PLATFORM
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import color as colorutil
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):
@ -64,6 +86,49 @@ class TuyaLight(TuyaDevice, LightEntity):
"""Init Tuya light device."""
super().__init__(tuya, platform)
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
def brightness(self):
@ -93,12 +158,12 @@ class TuyaLight(TuyaDevice, LightEntity):
@property
def min_mireds(self):
"""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
def max_mireds(self):
"""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):
"""Turn on or control the light."""

View file

@ -10,8 +10,6 @@ from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW
ENTITY_ID_FORMAT = SENSOR_DOMAIN + ".{}"
PARALLEL_UPDATES = 0
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up tuya sensors dynamically through tuya discovery."""

View file

@ -21,5 +21,41 @@
"error": {
"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"
}
}
}

View file

@ -1,4 +1,6 @@
"""Support for Tuya switches."""
from datetime import timedelta
from homeassistant.components.switch import (
DOMAIN as SENSOR_DOMAIN,
ENTITY_ID_FORMAT,
@ -10,7 +12,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import TuyaDevice
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):

View file

@ -1,14 +1,11 @@
{
"config": {
"abort": {
"auth_failed": "Invalid authentication",
"cannot_connect": "Failed to connect",
"conn_error": "Failed to connect",
"invalid_auth": "Invalid authentication",
"single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {
"auth_failed": "Invalid authentication",
"invalid_auth": "Invalid authentication"
},
"flow_title": "Tuya configuration",
@ -24,5 +21,41 @@
"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"
}
}
}
}
}