Bump tuyaha to 0.0.10 and fix set temperature issues (#45732)

This commit is contained in:
ollo69 2021-02-16 03:20:45 +01:00 committed by GitHub
parent 1bb535aa67
commit 3c26235e78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 318 additions and 52 deletions

View file

@ -993,7 +993,14 @@ omit =
homeassistant/components/transmission/const.py
homeassistant/components/transmission/errors.py
homeassistant/components/travisci/sensor.py
homeassistant/components/tuya/*
homeassistant/components/tuya/__init__.py
homeassistant/components/tuya/climate.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
homeassistant/components/twilio_call/notify.py

View file

@ -33,7 +33,9 @@ from .const import (
CONF_CURR_TEMP_DIVIDER,
CONF_MAX_TEMP,
CONF_MIN_TEMP,
CONF_SET_TEMP_DIVIDED,
CONF_TEMP_DIVIDER,
CONF_TEMP_STEP_OVERRIDE,
DOMAIN,
SIGNAL_CONFIG_ENTITY,
TUYA_DATA,
@ -103,6 +105,8 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
self.operations = [HVAC_MODE_OFF]
self._has_operation = False
self._def_hvac_mode = HVAC_MODE_AUTO
self._set_temp_divided = True
self._temp_step_override = None
self._min_temp = None
self._max_temp = None
@ -117,6 +121,8 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
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)
self._set_temp_divided = config.get(CONF_SET_TEMP_DIVIDED, True)
self._temp_step_override = config.get(CONF_TEMP_STEP_OVERRIDE)
min_temp = config.get(CONF_MIN_TEMP, 0)
max_temp = config.get(CONF_MAX_TEMP, 0)
if min_temp >= max_temp:
@ -189,6 +195,8 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
if self._temp_step_override:
return self._temp_step_override
return self._tuya.target_temperature_step()
@property
@ -204,7 +212,7 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity):
def set_temperature(self, **kwargs):
"""Set new target temperature."""
if ATTR_TEMPERATURE in kwargs:
self._tuya.set_temperature(kwargs[ATTR_TEMPERATURE])
self._tuya.set_temperature(kwargs[ATTR_TEMPERATURE], self._set_temp_divided)
def set_fan_mode(self, fan_mode):
"""Set new target fan mode."""

View file

@ -23,7 +23,6 @@ from homeassistant.const import (
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
# pylint:disable=unused-import
from .const import (
CONF_BRIGHTNESS_RANGE_MODE,
CONF_COUNTRYCODE,
@ -35,17 +34,19 @@ from .const import (
CONF_MIN_TEMP,
CONF_QUERY_DEVICE,
CONF_QUERY_INTERVAL,
CONF_SET_TEMP_DIVIDED,
CONF_SUPPORT_COLOR,
CONF_TEMP_DIVIDER,
CONF_TEMP_STEP_OVERRIDE,
CONF_TUYA_MAX_COLTEMP,
DEFAULT_DISCOVERY_INTERVAL,
DEFAULT_QUERY_INTERVAL,
DEFAULT_TUYA_MAX_COLTEMP,
DOMAIN,
TUYA_DATA,
TUYA_PLATFORMS,
TUYA_TYPE_NOT_QUERY,
)
from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
@ -66,6 +67,7 @@ ERROR_DEV_NOT_FOUND = "dev_not_found"
RESULT_AUTH_FAILED = "invalid_auth"
RESULT_CONN_ERROR = "cannot_connect"
RESULT_SINGLE_INSTANCE = "single_instance_allowed"
RESULT_SUCCESS = "success"
RESULT_LOG_MESSAGE = {
@ -123,7 +125,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return self.async_abort(reason=RESULT_SINGLE_INSTANCE)
errors = {}
@ -257,7 +259,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
if self.config_entry.state != config_entries.ENTRY_STATE_LOADED:
_LOGGER.error("Tuya integration not yet loaded")
return self.async_abort(reason="cannot_connect")
return self.async_abort(reason=RESULT_CONN_ERROR)
if user_input is not None:
dev_ids = user_input.get(CONF_LIST_DEVICES)
@ -323,11 +325,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
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":
return self._get_climate_schema(curr_conf, device)
if device_type != device.device_type():
return None
schema = None
if device_type == "light":
schema = self._get_light_schema(curr_conf, device)
elif device_type == "climate":
schema = self._get_climate_schema(curr_conf, device)
return schema
@staticmethod
def _get_light_schema(curr_conf, device):
@ -374,6 +379,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
"""Create option schema for climate device."""
unit = device.temperature_unit()
def_unit = TEMP_FAHRENHEIT if unit == "FAHRENHEIT" else TEMP_CELSIUS
supported_steps = device.supported_temperature_steps()
default_step = device.target_temperature_step()
config_schema = vol.Schema(
{
@ -389,6 +396,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
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_SET_TEMP_DIVIDED,
default=curr_conf.get(CONF_SET_TEMP_DIVIDED, True),
): bool,
vol.Optional(
CONF_TEMP_STEP_OVERRIDE,
default=curr_conf.get(CONF_TEMP_STEP_OVERRIDE, default_step),
): vol.In(supported_steps),
vol.Optional(
CONF_MIN_TEMP,
default=curr_conf.get(CONF_MIN_TEMP, 0),

View file

@ -10,8 +10,10 @@ CONF_MIN_KELVIN = "min_kelvin"
CONF_MIN_TEMP = "min_temp"
CONF_QUERY_DEVICE = "query_device"
CONF_QUERY_INTERVAL = "query_interval"
CONF_SET_TEMP_DIVIDED = "set_temp_divided"
CONF_SUPPORT_COLOR = "support_color"
CONF_TEMP_DIVIDER = "temp_divider"
CONF_TEMP_STEP_OVERRIDE = "temp_step_override"
CONF_TUYA_MAX_COLTEMP = "tuya_max_coltemp"
DEFAULT_DISCOVERY_INTERVAL = 605

View file

@ -2,7 +2,7 @@
"domain": "tuya",
"name": "Tuya",
"documentation": "https://www.home-assistant.io/integrations/tuya",
"requirements": ["tuyaha==0.0.9"],
"requirements": ["tuyaha==0.0.10"],
"codeowners": ["@ollo69"],
"config_flow": true
}

View file

@ -49,6 +49,8 @@
"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)",
"set_temp_divided": "Use divided Temperature value for set temperature command",
"temp_step_override": "Target Temperature step",
"min_temp": "Min target temperature (use min and max = 0 for default)",
"max_temp": "Max target temperature (use min and max = 0 for default)"
}

View file

@ -40,8 +40,10 @@
"max_temp": "Max target temperature (use min and max = 0 for default)",
"min_kelvin": "Min color temperature supported in kelvin",
"min_temp": "Min target temperature (use min and max = 0 for default)",
"set_temp_divided": "Use divided Temperature value for set temperature command",
"support_color": "Force color support",
"temp_divider": "Temperature values divider (0 = use default)",
"temp_step_override": "Target Temperature step",
"tuya_max_coltemp": "Max color temperature reported by device",
"unit_of_measurement": "Temperature unit used by device"
},

View file

@ -2217,7 +2217,7 @@ tp-connected==0.0.4
transmissionrpc==0.11
# homeassistant.components.tuya
tuyaha==0.0.9
tuyaha==0.0.10
# homeassistant.components.twentemilieu
twentemilieu==0.3.0

View file

@ -1126,7 +1126,7 @@ total_connect_client==0.55
transmissionrpc==0.11
# homeassistant.components.tuya
tuyaha==0.0.9
tuyaha==0.0.10
# homeassistant.components.twentemilieu
twentemilieu==0.3.0

View file

@ -0,0 +1,75 @@
"""Test code shared between test files."""
from tuyaha.devices import climate, light, switch
CLIMATE_ID = "1"
CLIMATE_DATA = {
"data": {"state": "true", "temp_unit": climate.UNIT_CELSIUS},
"id": CLIMATE_ID,
"ha_type": "climate",
"name": "TestClimate",
"dev_type": "climate",
}
LIGHT_ID = "2"
LIGHT_DATA = {
"data": {"state": "true"},
"id": LIGHT_ID,
"ha_type": "light",
"name": "TestLight",
"dev_type": "light",
}
SWITCH_ID = "3"
SWITCH_DATA = {
"data": {"state": True},
"id": SWITCH_ID,
"ha_type": "switch",
"name": "TestSwitch",
"dev_type": "switch",
}
LIGHT_ID_FAKE1 = "9998"
LIGHT_DATA_FAKE1 = {
"data": {"state": "true"},
"id": LIGHT_ID_FAKE1,
"ha_type": "light",
"name": "TestLightFake1",
"dev_type": "light",
}
LIGHT_ID_FAKE2 = "9999"
LIGHT_DATA_FAKE2 = {
"data": {"state": "true"},
"id": LIGHT_ID_FAKE2,
"ha_type": "light",
"name": "TestLightFake2",
"dev_type": "light",
}
TUYA_DEVICES = [
climate.TuyaClimate(CLIMATE_DATA, None),
light.TuyaLight(LIGHT_DATA, None),
switch.TuyaSwitch(SWITCH_DATA, None),
light.TuyaLight(LIGHT_DATA_FAKE1, None),
light.TuyaLight(LIGHT_DATA_FAKE2, None),
]
class MockTuya:
"""Mock for Tuya devices."""
def get_all_devices(self):
"""Return all configured devices."""
return TUYA_DEVICES
def get_device_by_id(self, dev_id):
"""Return configured device with dev id."""
if dev_id == LIGHT_ID_FAKE1:
return None
if dev_id == LIGHT_ID_FAKE2:
return switch.TuyaSwitch(SWITCH_DATA, None)
for device in TUYA_DEVICES:
if device.object_id() == dev_id:
return device
return None

View file

@ -2,11 +2,47 @@
from unittest.mock import Mock, patch
import pytest
from tuyaha.devices.climate import STEP_HALVES
from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.tuya.const import CONF_COUNTRYCODE, DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.tuya.config_flow import (
CONF_LIST_DEVICES,
ERROR_DEV_MULTI_TYPE,
ERROR_DEV_NOT_CONFIG,
ERROR_DEV_NOT_FOUND,
RESULT_AUTH_FAILED,
RESULT_CONN_ERROR,
RESULT_SINGLE_INSTANCE,
)
from homeassistant.components.tuya.const import (
CONF_BRIGHTNESS_RANGE_MODE,
CONF_COUNTRYCODE,
CONF_CURR_TEMP_DIVIDER,
CONF_DISCOVERY_INTERVAL,
CONF_MAX_KELVIN,
CONF_MAX_TEMP,
CONF_MIN_KELVIN,
CONF_MIN_TEMP,
CONF_QUERY_DEVICE,
CONF_QUERY_INTERVAL,
CONF_SET_TEMP_DIVIDED,
CONF_SUPPORT_COLOR,
CONF_TEMP_DIVIDER,
CONF_TEMP_STEP_OVERRIDE,
CONF_TUYA_MAX_COLTEMP,
DOMAIN,
TUYA_DATA,
)
from homeassistant.const import (
CONF_PASSWORD,
CONF_PLATFORM,
CONF_UNIT_OF_MEASUREMENT,
CONF_USERNAME,
TEMP_CELSIUS,
)
from .common import CLIMATE_ID, LIGHT_ID, LIGHT_ID_FAKE1, LIGHT_ID_FAKE2, MockTuya
from tests.common import MockConfigEntry
@ -30,9 +66,15 @@ def tuya_fixture() -> Mock:
yield tuya
@pytest.fixture(name="tuya_setup", autouse=True)
def tuya_setup_fixture():
"""Mock tuya entry setup."""
with patch("homeassistant.components.tuya.async_setup_entry", return_value=True):
yield
async def test_user(hass, tuya):
"""Test user config."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
@ -40,11 +82,6 @@ async def test_user(hass, tuya):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
with patch(
"homeassistant.components.tuya.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.tuya.async_setup_entry", return_value=True
) as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=TUYA_USER_DATA
)
@ -58,20 +95,9 @@ async def test_user(hass, tuya):
assert result["data"][CONF_PLATFORM] == TUYA_PLATFORM
assert not result["result"].unique_id
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_import(hass, tuya):
"""Test import step."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.tuya.async_setup",
return_value=True,
) as mock_setup, patch(
"homeassistant.components.tuya.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
@ -87,9 +113,6 @@ async def test_import(hass, tuya):
assert result["data"][CONF_PLATFORM] == TUYA_PLATFORM
assert not result["result"].unique_id
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_abort_if_already_setup(hass, tuya):
"""Test we abort if Tuya is already setup."""
@ -101,7 +124,7 @@ async def test_abort_if_already_setup(hass, tuya):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "single_instance_allowed"
assert result["reason"] == RESULT_SINGLE_INSTANCE
# Should fail, config exist (flow)
result = await hass.config_entries.flow.async_init(
@ -109,7 +132,7 @@ async def test_abort_if_already_setup(hass, tuya):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "single_instance_allowed"
assert result["reason"] == RESULT_SINGLE_INSTANCE
async def test_abort_on_invalid_credentials(hass, tuya):
@ -121,14 +144,14 @@ async def test_abort_on_invalid_credentials(hass, tuya):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": "invalid_auth"}
assert result["errors"] == {"base": RESULT_AUTH_FAILED}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "invalid_auth"
assert result["reason"] == RESULT_AUTH_FAILED
async def test_abort_on_connection_error(hass, tuya):
@ -140,11 +163,143 @@ async def test_abort_on_connection_error(hass, tuya):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "cannot_connect"
assert result["reason"] == RESULT_CONN_ERROR
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "cannot_connect"
assert result["reason"] == RESULT_CONN_ERROR
async def test_options_flow(hass):
"""Test config flow options."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data=TUYA_USER_DATA,
)
config_entry.add_to_hass(hass)
# Test check for integration not loaded
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == RESULT_CONN_ERROR
# Load integration and enter options
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
hass.data[DOMAIN] = {TUYA_DATA: MockTuya()}
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
# Test dev not found error
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={CONF_LIST_DEVICES: [f"light-{LIGHT_ID_FAKE1}"]},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
assert result["errors"] == {"base": ERROR_DEV_NOT_FOUND}
# Test dev type error
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={CONF_LIST_DEVICES: [f"light-{LIGHT_ID_FAKE2}"]},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
assert result["errors"] == {"base": ERROR_DEV_NOT_CONFIG}
# Test multi dev error
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={CONF_LIST_DEVICES: [f"climate-{CLIMATE_ID}", f"light-{LIGHT_ID}"]},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
assert result["errors"] == {"base": ERROR_DEV_MULTI_TYPE}
# Test climate options form
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={CONF_LIST_DEVICES: [f"climate-{CLIMATE_ID}"]}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "device"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
CONF_TEMP_DIVIDER: 10,
CONF_CURR_TEMP_DIVIDER: 5,
CONF_SET_TEMP_DIVIDED: False,
CONF_TEMP_STEP_OVERRIDE: STEP_HALVES,
CONF_MIN_TEMP: 12,
CONF_MAX_TEMP: 22,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
# Test light options form
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={CONF_LIST_DEVICES: [f"light-{LIGHT_ID}"]}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "device"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_SUPPORT_COLOR: True,
CONF_BRIGHTNESS_RANGE_MODE: 1,
CONF_MIN_KELVIN: 4000,
CONF_MAX_KELVIN: 5000,
CONF_TUYA_MAX_COLTEMP: 12000,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
# Test common options
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_DISCOVERY_INTERVAL: 100,
CONF_QUERY_INTERVAL: 50,
CONF_QUERY_DEVICE: LIGHT_ID,
},
)
# Verify results
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
climate_options = config_entry.options[CLIMATE_ID]
assert climate_options[CONF_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
assert climate_options[CONF_TEMP_DIVIDER] == 10
assert climate_options[CONF_CURR_TEMP_DIVIDER] == 5
assert climate_options[CONF_SET_TEMP_DIVIDED] is False
assert climate_options[CONF_TEMP_STEP_OVERRIDE] == STEP_HALVES
assert climate_options[CONF_MIN_TEMP] == 12
assert climate_options[CONF_MAX_TEMP] == 22
light_options = config_entry.options[LIGHT_ID]
assert light_options[CONF_SUPPORT_COLOR] is True
assert light_options[CONF_BRIGHTNESS_RANGE_MODE] == 1
assert light_options[CONF_MIN_KELVIN] == 4000
assert light_options[CONF_MAX_KELVIN] == 5000
assert light_options[CONF_TUYA_MAX_COLTEMP] == 12000
assert config_entry.options[CONF_DISCOVERY_INTERVAL] == 100
assert config_entry.options[CONF_QUERY_INTERVAL] == 50
assert config_entry.options[CONF_QUERY_DEVICE] == LIGHT_ID