diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index f981582..167490f 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -47,6 +47,15 @@ DATA_DISCOVERY = "discovery" DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["binary_sensor", "cover", "fan", "light", "sensor", "switch"] +PLATFORMS = [ + "binary_sensor", + "cover", + "fan", + "light", + "number", + "select", + "sensor", + "switch", +] TUYA_DEVICE = "tuya_device" diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 834e199..0c65dfa 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -79,7 +79,13 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Get the list of available speeds.""" return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - async def async_turn_on(self, speed: str = None, **kwargs) -> None: + async def async_turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Turn on the entity.""" await self._device.set_dp(True, self._dp_id) if speed is not None: diff --git a/custom_components/localtuya/number.py b/custom_components/localtuya/number.py new file mode 100644 index 0000000..328f6a2 --- /dev/null +++ b/custom_components/localtuya/number.py @@ -0,0 +1,87 @@ +"""Platform to present any Tuya DP as a number.""" +import logging +from functools import partial + +import voluptuous as vol +from homeassistant.components.number import DOMAIN, NumberEntity +from homeassistant.const import ( + CONF_DEVICE_CLASS, + STATE_UNKNOWN, +) + +from .common import LocalTuyaEntity, async_setup_entry + +_LOGGER = logging.getLogger(__name__) + +CONF_MIN_VALUE = "min_value" +CONF_MAX_VALUE = "max_value" + +DEFAULT_MIN = 0 +DEFAULT_MAX = 100000 + + +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Optional(CONF_MIN_VALUE, default=DEFAULT_MIN): vol.All( + vol.Coerce(float), + vol.Range(min=-1000000.0, max=1000000.0), + ), + vol.Required(CONF_MAX_VALUE, default=DEFAULT_MAX): vol.All( + vol.Coerce(float), + vol.Range(min=-1000000.0, max=1000000.0), + ), + } + + +class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): + """Representation of a Tuya Number.""" + + def __init__( + self, + device, + config_entry, + sensorid, + **kwargs, + ): + """Initialize the Tuya sensor.""" + super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs) + self._state = STATE_UNKNOWN + + self._min_value = DEFAULT_MIN + if CONF_MIN_VALUE in self._config: + self._min_value = self._config.get(CONF_MIN_VALUE) + + self._max_value = self._config.get(CONF_MAX_VALUE) + + @property + def value(self) -> float: + """Return sensor state.""" + return self._state + + @property + def min_value(self) -> float: + """Return the minimum value.""" + return self._min_value + + @property + def max_value(self) -> float: + """Return the maximum value.""" + return self._max_value + + @property + def device_class(self): + """Return the class of this device.""" + return self._config.get(CONF_DEVICE_CLASS) + + async def async_set_value(self, value: float) -> None: + """Update the current value.""" + await self._device.set_dp(value, self._dp_id) + + def status_updated(self): + """Device status was updated.""" + state = self.dps(self._dp_id) + self._state = state + + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaNumber, flow_schema) diff --git a/custom_components/localtuya/select.py b/custom_components/localtuya/select.py new file mode 100644 index 0000000..43b32c9 --- /dev/null +++ b/custom_components/localtuya/select.py @@ -0,0 +1,103 @@ +"""Platform to present any Tuya DP as an enumeration.""" +import logging +from functools import partial + +import voluptuous as vol +from homeassistant.components.select import DOMAIN, SelectEntity +from homeassistant.const import ( + CONF_DEVICE_CLASS, + STATE_UNKNOWN, +) + +from .common import LocalTuyaEntity, async_setup_entry + +_LOGGER = logging.getLogger(__name__) + +CONF_OPTIONS = "select_options" +CONF_OPTIONS_FRIENDLY = "select_options_friendly" + + +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Required(CONF_OPTIONS): str, + vol.Optional(CONF_OPTIONS_FRIENDLY): str, + } + + +class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): + """Representation of a Tuya Enumeration.""" + + def __init__( + self, + device, + config_entry, + sensorid, + **kwargs, + ): + """Initialize the Tuya sensor.""" + super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs) + self._state = STATE_UNKNOWN + self._state_friendly = "" + self._valid_options = self._config.get(CONF_OPTIONS).split(";") + + # Set Display options + self._display_options = [] + display_options_str = "" + if CONF_OPTIONS_FRIENDLY in self._config: + display_options_str = self._config.get(CONF_OPTIONS_FRIENDLY).strip() + _LOGGER.debug("Display Options Configured: %s", display_options_str) + + if display_options_str.find(";") >= 0: + self._display_options = display_options_str.split(";") + elif len(display_options_str.strip()) > 0: + self._display_options.append(display_options_str) + else: + # Default display string to raw string + _LOGGER.debug("No Display options configured - defaulting to raw values") + self._display_options = self._valid_options + + _LOGGER.debug( + "Total Raw Options: %s - Total Display Options: %s", + str(len(self._valid_options)), + str(len(self._display_options)), + ) + if len(self._valid_options) > len(self._display_options): + # If list of display items smaller than list of valid items, + # then default remaining items to be the raw value + _LOGGER.debug( + "Valid options is larger than display options - \ + filling up with raw values" + ) + for i in range(len(self._display_options), len(self._valid_options)): + self._display_options.append(self._valid_options[i]) + + @property + def current_option(self) -> str: + """Return the current value.""" + return self._state_friendly + + @property + def options(self) -> list: + """Return the list of values.""" + return self._display_options + + @property + def device_class(self): + """Return the class of this device.""" + return self._config.get(CONF_DEVICE_CLASS) + + async def async_select_option(self, option: str) -> None: + """Update the current value.""" + option_value = self._valid_options[self._display_options.index(option)] + _LOGGER.debug("Sending Option: " + option + " -> " + option_value) + await self._device.set_dp(option_value, self._dp_id) + + def status_updated(self): + """Device status was updated.""" + state = self.dps(self._dp_id) + self._state_friendly = self._display_options[self._valid_options.index(state)] + self._state = state + + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaSelect, flow_schema) diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 6ce233a..f12c4ab 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -74,7 +74,11 @@ "fan_oscillating_control": "Fan Oscillating Control", "fan_speed_low": "Fan Low Speed Setting", "fan_speed_medium": "Fan Medium Speed Setting", - "fan_speed_high": "Fan High Speed Setting" + "fan_speed_high": "Fan High Speed Setting", + "max_value": "Maximum Value", + "min_value": "Minimum Value", + "select_options": "Valid entries, separate entries by a ;", + "select_options_friendly": "User Friendly options, separate entries by a ;" } } } @@ -126,7 +130,11 @@ "fan_oscillating_control": "Fan Oscillating Control", "fan_speed_low": "Fan Low Speed Setting", "fan_speed_medium": "Fan Medium Speed Setting", - "fan_speed_high": "Fan High Speed Setting" + "fan_speed_high": "Fan High Speed Setting", + "max_value": "Maximum Value", + "min_value": "Minimum Value", + "select_options": "Valid entries, separate entries by a ;", + "select_options_friendly": "User Friendly options, separate entries by a ;" } }, "yaml_import": { diff --git a/hacs.json b/hacs.json index 4b6ec05..3c5a41a 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "Local Tuya", - "domains": ["climate", "cover", "fan", "light", "sensor", "switch"], + "domains": ["climate", "cover", "fan", "light", "number", "select", "sensor", "switch"], "homeassistant": "0.116.0", "iot_class": ["Local Push"] } diff --git a/requirements_test.txt b/requirements_test.txt index df62e77..a4e209a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -3,7 +3,7 @@ codespell==2.0.0 flake8==3.9.2 mypy==0.901 pydocstyle==6.1.1 -cryptography==3.2 +cryptography==3.3.2 pylint==2.8.2 pylint-strict-informational==0.1 -homeassistant==2021.1.4 +homeassistant==2021.7.1 diff --git a/setup.cfg b/setup.cfg index 2d94fd3..562dc77 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,7 +12,7 @@ warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true -[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*] +[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,homeassistant.components.select.*,homeassistant.components.number.*] strict = true ignore_errors = false warn_unreachable = true