From b8333affc590da29b00d0554e03b3ea8826bfb67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Sun, 6 Sep 2020 10:29:39 +0200 Subject: [PATCH 01/17] Add support for config flows for switches --- .gitignore | 2 + custom_components/localtuya/__init__.py | 31 +++- custom_components/localtuya/config_flow.py | 140 ++++++++++++++++++ custom_components/localtuya/const.py | 12 ++ custom_components/localtuya/manifest.json | 17 ++- custom_components/localtuya/strings.json | 42 ++++++ custom_components/localtuya/switch.py | 54 +++++-- .../localtuya/translations/en.json | 42 ++++++ 8 files changed, 322 insertions(+), 18 deletions(-) create mode 100644 .gitignore create mode 100644 custom_components/localtuya/config_flow.py create mode 100644 custom_components/localtuya/const.py create mode 100644 custom_components/localtuya/strings.json create mode 100644 custom_components/localtuya/translations/en.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b5e2e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*~ +__pycache__ diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index e9abf36..62b2089 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -1 +1,30 @@ -"""The Tuya local integration.""" \ No newline at end of file +"""The LocalTuya integration integration.""" +import asyncio + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import CONF_DEVICE_TYPE, DOMAIN, DEVICE_TYPE_POWER_OUTLET + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the LocalTuya integration component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up LocalTuya integration from a config entry.""" + if entry.data[CONF_DEVICE_TYPE] == DEVICE_TYPE_POWER_OUTLET: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "switch") + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + # Nothing is stored and no persistent connections exist, so nothing to do + return True diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py new file mode 100644 index 0000000..c9e5795 --- /dev/null +++ b/custom_components/localtuya/config_flow.py @@ -0,0 +1,140 @@ +"""Config flow for LocalTuya integration integration.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import ( + CONF_SWITCHES, + CONF_ID, + CONF_HOST, + CONF_DEVICE_ID, + CONF_NAME, + CONF_FRIENDLY_NAME, +) +import homeassistant.helpers.config_validation as cv + +from . import pytuya +from .const import ( # pylint: disable=unused-import + CONF_DEVICE_TYPE, + CONF_LOCAL_KEY, + CONF_PROTOCOL_VERSION, + CONF_CURRENT, + CONF_CURRENT_CONSUMPTION, + CONF_VOLTAGE, + DOMAIN, + DEVICE_TYPE_POWER_OUTLET, +) + +_LOGGER = logging.getLogger(__name__) + +ADD_ANOTHER_SWITCH = "add_another_switch" + +USER_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): str, + vol.Required(CONF_HOST): str, + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_LOCAL_KEY): str, + vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]), + vol.Required(CONF_DEVICE_TYPE, default=DEVICE_TYPE_POWER_OUTLET): vol.In( + [DEVICE_TYPE_POWER_OUTLET] + ), + } +) + +POWER_OUTLET_SCHEMA = vol.Schema( + { + vol.Required(CONF_ID, default=1): int, + vol.Required(CONF_NAME): str, + vol.Required(CONF_FRIENDLY_NAME): str, + vol.Required(CONF_CURRENT, default=18): int, + vol.Required(CONF_CURRENT_CONSUMPTION, default=19): int, + vol.Required(CONF_VOLTAGE, default=20): int, + vol.Required(ADD_ANOTHER_SWITCH, default=False): bool, + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + pytuyadevice = pytuya.OutletDevice( + data[CONF_DEVICE_ID], data[CONF_HOST], data[CONF_LOCAL_KEY] + ) + pytuyadevice.set_version(float(data[CONF_PROTOCOL_VERSION])) + pytuyadevice.set_dpsUsed({"1": None}) + try: + await hass.async_add_executor_job(pytuyadevice.status) + except ConnectionRefusedError: + raise CannotConnect + except ValueError: + raise InvalidAuth + return data + + +class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for LocalTuya integration.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize a new LocaltuyaConfigFlow.""" + self.basic_info = None + self.switches = [] + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_DEVICE_ID]) + self._abort_if_unique_id_configured() + + try: + self.basic_info = await validate_input(self.hass, user_input) + if self.basic_info[CONF_DEVICE_TYPE] == "Power Outlet": + return await self.async_step_power_outlet() + + return self.async_abort(reason="unsupported_device_type") + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=USER_SCHEMA, errors=errors + ) + + async def async_step_power_outlet(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + already_configured = any( + switch[CONF_ID] == user_input[CONF_ID] for switch in self.switches + ) + if not already_configured: + add_another_switch = user_input.pop(ADD_ANOTHER_SWITCH) + self.switches.append(user_input) + if not add_another_switch: + config = {**self.basic_info, CONF_SWITCHES: self.switches} + return self.async_create_entry(title=config[CONF_NAME], data=config) + else: + errors["base"] = "switch_already_configured" + + return self.async_show_form( + step_id="power_outlet", + data_schema=POWER_OUTLET_SCHEMA, + errors=errors, + description_placeholders={"number": len(self.switches) + 1}, + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py new file mode 100644 index 0000000..3e463f7 --- /dev/null +++ b/custom_components/localtuya/const.py @@ -0,0 +1,12 @@ +"""Constants for localtuya integration.""" + +CONF_DEVICE_TYPE = "device_type" +CONF_LOCAL_KEY = "local_key" +CONF_PROTOCOL_VERSION = "protocol_version" +CONF_CURRENT = "current" +CONF_CURRENT_CONSUMPTION = "current_consumption" +CONF_VOLTAGE = "voltage" + +DOMAIN = "localtuya" + +DEVICE_TYPE_POWER_OUTLET = "Power Outlet" diff --git a/custom_components/localtuya/manifest.json b/custom_components/localtuya/manifest.json index 9b7297e..e6c7e86 100644 --- a/custom_components/localtuya/manifest.json +++ b/custom_components/localtuya/manifest.json @@ -1,8 +1,11 @@ -{ - "domain": "localtuya", - "name": "LocalTuya integration", - "documentation": "https://github.com/rospogrigio/localtuya-homeassistant/", - "dependencies": [], - "codeowners": ["@rospogrigio"], - "requirements": ["pycryptodome==3.9.8"] +{ + "domain": "localtuya", + "name": "LocalTuya integration", + "documentation": "https://github.com/rospogrigio/localtuya-homeassistant/", + "dependencies": [], + "codeowners": [ + "@rospogrigio" + ], + "requirements": ["pycryptodome==3.9.8"], + "config_flow": true } \ No newline at end of file diff --git a/custom_components/localtuya/strings.json b/custom_components/localtuya/strings.json new file mode 100644 index 0000000..898b99d --- /dev/null +++ b/custom_components/localtuya/strings.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Device has already been configured.", + "unsupported_device_type": "Unsupported device type!" + }, + "error": { + "cannot_connect": "Cannot connect to device. Verify that address is correct.", + "invalid_auth": "Failed to authenticate with device. Verify that device id and local key are correct.", + "unknown": "An unknown error occurred. See log for details.", + "switch_already_configured": "Switch with this ID has already been configured." + }, + "step": { + "user": { + "title": "Add Tuya device", + "description": "Fill in the basic details and pick device type. The name entered here will be used to identify the integration itself (as seen in the `Integrations` page). You will name each sub-device in the following steps.", + "data": { + "name": "Name", + "host": "Host", + "device_id": "Device ID", + "local_key": "Local key", + "protocol_version": "Protocol Version", + "device_type": "Device type" + } + }, + "power_outlet": { + "title": "Add subswitch", + "description": "You are about to add subswitch number `{number}`. If you want to add another, tick `Add another switch` before continuing.", + "data": { + "id": "ID", + "name": "Name", + "friendly_name": "Friendly name", + "current": "Current", + "current_consumption": "Current Consumption", + "voltage": "Voltage", + "add_another_switch": "Add another switch" + } + } + } + }, + "title": "LocalTuya" +} \ No newline at end of file diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index ab6cf98..286cfad 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -28,22 +28,18 @@ import logging import voluptuous as vol from homeassistant.components.switch import SwitchEntity, PLATFORM_SCHEMA -from homeassistant.const import (CONF_HOST, CONF_ID, CONF_SWITCHES, CONF_FRIENDLY_NAME, CONF_ICON, CONF_NAME) +from homeassistant.const import (CONF_HOST, CONF_ID, CONF_SWITCHES, CONF_DEVICE_ID, CONF_FRIENDLY_NAME, CONF_ICON, CONF_NAME) import homeassistant.helpers.config_validation as cv from time import time, sleep from threading import Lock +from . import pytuya +from .const import CONF_LOCAL_KEY, CONF_PROTOCOL_VERSION, CONF_CURRENT, CONF_CURRENT_CONSUMPTION, CONF_VOLTAGE + _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['pytuya==7.0.9'] -CONF_DEVICE_ID = 'device_id' -CONF_LOCAL_KEY = 'local_key' -CONF_PROTOCOL_VERSION = 'protocol_version' -CONF_CURRENT = 'current' -CONF_CURRENT_CONSUMPTION = 'current_consumption' -CONF_VOLTAGE = 'voltage' - DEFAULT_ID = '1' DEFAULT_PROTOCOL_VERSION = 3.3 @@ -77,6 +73,43 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Setup a Tuya switch based on a config entry.""" + switches = [] + pytuyadevice = pytuya.OutletDevice( + config_entry.data[CONF_DEVICE_ID], + config_entry.data[CONF_HOST], + config_entry.data[CONF_LOCAL_KEY]) + pytuyadevice.set_version(float(config_entry.data[CONF_PROTOCOL_VERSION])) + dps = {} + + for device_config in config_entry.data[CONF_SWITCHES]: + dps[str(device_config[CONF_ID])] = None + if device_config[CONF_CURRENT] != 0: + dps[str(device_config[CONF_CURRENT])] = None + if device_config[CONF_CURRENT_CONSUMPTION] != 0: + dps[str(device_config[CONF_CURRENT_CONSUMPTION])] = None + if device_config[CONF_VOLTAGE] != 0: + dps[str(device_config[CONF_VOLTAGE])] = None + + switches.append( + TuyaDevice( + TuyaCache(pytuyadevice), + device_config[CONF_NAME], + device_config[CONF_FRIENDLY_NAME], + None, # Icon + str(device_config[CONF_ID]), + str(device_config[CONF_CURRENT]), + str(device_config[CONF_CURRENT_CONSUMPTION]), + str(device_config[CONF_VOLTAGE]) + ) + ) + + pytuyadevice.set_dpsUsed(dps) + + async_add_entities(switches, True) + + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up of the Tuya switch.""" from . import pytuya @@ -199,6 +232,7 @@ class TuyaCache: finally: self._lock.release() + class TuyaDevice(SwitchEntity): """Representation of a Tuya switch.""" @@ -212,8 +246,8 @@ class TuyaDevice(SwitchEntity): self._attr_current = attr_current self._attr_consumption = attr_consumption self._attr_voltage = attr_voltage - self._status = self._device.status() - self._state = self._status['dps'][self._switch_id] + self._status = None + self._state = None print('Initialized tuya switch [{}] with switch status [{}] and state [{}]'.format(self._name, self._status, self._state)) @property diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json new file mode 100644 index 0000000..898b99d --- /dev/null +++ b/custom_components/localtuya/translations/en.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Device has already been configured.", + "unsupported_device_type": "Unsupported device type!" + }, + "error": { + "cannot_connect": "Cannot connect to device. Verify that address is correct.", + "invalid_auth": "Failed to authenticate with device. Verify that device id and local key are correct.", + "unknown": "An unknown error occurred. See log for details.", + "switch_already_configured": "Switch with this ID has already been configured." + }, + "step": { + "user": { + "title": "Add Tuya device", + "description": "Fill in the basic details and pick device type. The name entered here will be used to identify the integration itself (as seen in the `Integrations` page). You will name each sub-device in the following steps.", + "data": { + "name": "Name", + "host": "Host", + "device_id": "Device ID", + "local_key": "Local key", + "protocol_version": "Protocol Version", + "device_type": "Device type" + } + }, + "power_outlet": { + "title": "Add subswitch", + "description": "You are about to add subswitch number `{number}`. If you want to add another, tick `Add another switch` before continuing.", + "data": { + "id": "ID", + "name": "Name", + "friendly_name": "Friendly name", + "current": "Current", + "current_consumption": "Current Consumption", + "voltage": "Voltage", + "add_another_switch": "Add another switch" + } + } + } + }, + "title": "LocalTuya" +} \ No newline at end of file From 0600b2730b1b6e2c6625a8195c3f247dae8e9633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Tue, 8 Sep 2020 11:59:39 +0200 Subject: [PATCH 02/17] Make config flows general based on entities --- custom_components/localtuya/__init__.py | 8 +- custom_components/localtuya/config_flow.py | 162 +++++++---- custom_components/localtuya/const.py | 8 +- custom_components/localtuya/switch.py | 269 +++++++++--------- .../localtuya/translations/en.json | 30 +- 5 files changed, 271 insertions(+), 206 deletions(-) diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index 62b2089..f3c215a 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -5,8 +5,9 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_PLATFORM -from .const import CONF_DEVICE_TYPE, DOMAIN, DEVICE_TYPE_POWER_OUTLET +from .const import DOMAIN, PLATFORMS async def async_setup(hass: HomeAssistant, config: dict): @@ -16,11 +17,10 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up LocalTuya integration from a config entry.""" - if entry.data[CONF_DEVICE_TYPE] == DEVICE_TYPE_POWER_OUTLET: + for component in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "switch") + hass.config_entries.async_forward_entry_setup(entry, component) ) - return True diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index c9e5795..76e3060 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -1,75 +1,81 @@ """Config flow for LocalTuya integration integration.""" import logging +from importlib import import_module import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import ( - CONF_SWITCHES, + CONF_ENTITIES, CONF_ID, CONF_HOST, CONF_DEVICE_ID, CONF_NAME, CONF_FRIENDLY_NAME, + CONF_PLATFORM, + CONF_SWITCHES, ) -import homeassistant.helpers.config_validation as cv from . import pytuya from .const import ( # pylint: disable=unused-import - CONF_DEVICE_TYPE, CONF_LOCAL_KEY, CONF_PROTOCOL_VERSION, - CONF_CURRENT, - CONF_CURRENT_CONSUMPTION, - CONF_VOLTAGE, DOMAIN, - DEVICE_TYPE_POWER_OUTLET, + PLATFORMS, ) _LOGGER = logging.getLogger(__name__) -ADD_ANOTHER_SWITCH = "add_another_switch" +PLATFORM_TO_ADD = "platform_to_add" +NO_ADDITIONAL_PLATFORMS = "no_additional_platforms" USER_SCHEMA = vol.Schema( { - vol.Required(CONF_NAME): str, - vol.Required(CONF_HOST): str, - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_LOCAL_KEY): str, + vol.Required(CONF_NAME, default="bla"): str, + vol.Required(CONF_HOST, default="10.0.10.110"): str, + vol.Required(CONF_DEVICE_ID, default="307001182462ab4de00b"): str, + vol.Required(CONF_LOCAL_KEY, default="28ac0651f2d187df"): str, vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]), - vol.Required(CONF_DEVICE_TYPE, default=DEVICE_TYPE_POWER_OUTLET): vol.In( - [DEVICE_TYPE_POWER_OUTLET] - ), } ) -POWER_OUTLET_SCHEMA = vol.Schema( - { - vol.Required(CONF_ID, default=1): int, - vol.Required(CONF_NAME): str, - vol.Required(CONF_FRIENDLY_NAME): str, - vol.Required(CONF_CURRENT, default=18): int, - vol.Required(CONF_CURRENT_CONSUMPTION, default=19): int, - vol.Required(CONF_VOLTAGE, default=20): int, - vol.Required(ADD_ANOTHER_SWITCH, default=False): bool, - } +PICK_ENTITY_SCHEMA = vol.Schema( + {vol.Required(PLATFORM_TO_ADD, default=PLATFORMS[0]): vol.In(PLATFORMS)} ) +def platform_schema(dps, additional_fields): + """Generate input validation schema for a platform.""" + dps_list = vol.In([f"{id} (value: {value})" for id, value in dps.items()]) + return vol.Schema( + { + vol.Required(CONF_ID): dps_list, + vol.Required(CONF_FRIENDLY_NAME): str, + } + ).extend({conf: dps_list for conf in additional_fields}) + + +def strip_dps_values(user_input, fields): + """Remove values and keep only index for DPS config items.""" + for field in [CONF_ID] + fields: + user_input[field] = user_input[field].split(" ")[0] + return user_input + + async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" pytuyadevice = pytuya.OutletDevice( data[CONF_DEVICE_ID], data[CONF_HOST], data[CONF_LOCAL_KEY] ) pytuyadevice.set_version(float(data[CONF_PROTOCOL_VERSION])) - pytuyadevice.set_dpsUsed({"1": None}) + pytuyadevice.set_dpsUsed({}) try: - await hass.async_add_executor_job(pytuyadevice.status) - except ConnectionRefusedError: + data = await hass.async_add_executor_job(pytuyadevice.status) + except (ConnectionRefusedError, ConnectionResetError): raise CannotConnect except ValueError: raise InvalidAuth - return data + return data["dps"] class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -81,7 +87,10 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize a new LocaltuyaConfigFlow.""" self.basic_info = None - self.switches = [] + self.dps_data = None + self.platform = None + self.platform_dps_fields = None + self.entities = [] async def async_step_user(self, user_input=None): """Handle the initial step.""" @@ -91,11 +100,9 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() try: - self.basic_info = await validate_input(self.hass, user_input) - if self.basic_info[CONF_DEVICE_TYPE] == "Power Outlet": - return await self.async_step_power_outlet() - - return self.async_abort(reason="unsupported_device_type") + self.basic_info = user_input + self.dps_data = await validate_input(self.hass, user_input) + return await self.async_step_pick_entity_type() except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: @@ -108,29 +115,88 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=USER_SCHEMA, errors=errors ) - async def async_step_power_outlet(self, user_input=None): - """Handle the initial step.""" + async def async_step_pick_entity_type(self, user_input=None): + """Handle asking if user wants to add another entity.""" + if user_input is not None: + if user_input.get(NO_ADDITIONAL_PLATFORMS): + config = {**self.basic_info, CONF_ENTITIES: self.entities} + return self.async_create_entry(title=config[CONF_NAME], data=config) + + self._set_platform(user_input[PLATFORM_TO_ADD]) + return await self.async_step_add_entity() + + # Add a checkbox that allows bailing out from config flow iff at least one + # entity has been added + schema = PICK_ENTITY_SCHEMA + if self.platform is not None: + schema = schema.extend( + {vol.Required(NO_ADDITIONAL_PLATFORMS, default=True): bool} + ) + + return self.async_show_form(step_id="pick_entity_type", data_schema=schema) + + async def async_step_add_entity(self, user_input=None): + """Handle adding a new entity.""" errors = {} if user_input is not None: already_configured = any( - switch[CONF_ID] == user_input[CONF_ID] for switch in self.switches + switch[CONF_ID] == user_input[CONF_ID] for switch in self.entities ) if not already_configured: - add_another_switch = user_input.pop(ADD_ANOTHER_SWITCH) - self.switches.append(user_input) - if not add_another_switch: - config = {**self.basic_info, CONF_SWITCHES: self.switches} - return self.async_create_entry(title=config[CONF_NAME], data=config) - else: - errors["base"] = "switch_already_configured" + user_input[CONF_PLATFORM] = self.platform + self.entities.append( + strip_dps_values(user_input, self.platform_dps_fields) + ) + return await self.async_step_pick_entity_type() + + errors["base"] = "entity_already_configured" return self.async_show_form( - step_id="power_outlet", - data_schema=POWER_OUTLET_SCHEMA, + step_id="add_entity", + data_schema=platform_schema(self.dps_data, self.platform_dps_fields), errors=errors, - description_placeholders={"number": len(self.switches) + 1}, + description_placeholders={"platform": self.platform}, ) + async def async_step_import(self, user_input): + """Handle import from YAML.""" + + def _convert_entity(conf): + converted = { + CONF_ID: conf[CONF_ID], + CONF_FRIENDLY_NAME: conf[CONF_FRIENDLY_NAME], + CONF_PLATFORM: self.platform, + } + for field in self.platform_dps_fields: + converted[str(field)] = conf[field] + return converted + + await self.async_set_unique_id(user_input[CONF_DEVICE_ID]) + self._set_platform(user_input[CONF_PLATFORM]) + + if len(user_input[CONF_SWITCHES]) > 0: + for switch_conf in user_input[CONF_SWITCHES].values(): + self.entities.append(_convert_entity(switch_conf)) + else: + self.entities.append(_convert_entity(user_input)) + + config = { + CONF_NAME: f"{user_input[CONF_DEVICE_ID]} (import from configuration.yaml)", + CONF_HOST: user_input[CONF_HOST], + CONF_DEVICE_ID: user_input[CONF_DEVICE_ID], + CONF_LOCAL_KEY: user_input[CONF_LOCAL_KEY], + CONF_PROTOCOL_VERSION: user_input[CONF_PROTOCOL_VERSION], + CONF_ENTITIES: self.entities, + } + self._abort_if_unique_id_configured(updates=config) + return self.async_create_entry(title=config[CONF_NAME], data=config) + + def _set_platform(self, platform): + self.platform = platform + self.platform_dps_fields = import_module( + f"homeassistant.components.localtuya.{platform}" + ).DPS_FIELDS + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 3e463f7..9361216 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -1,6 +1,9 @@ """Constants for localtuya integration.""" -CONF_DEVICE_TYPE = "device_type" +ATTR_CURRENT = 'current' +ATTR_CURRENT_CONSUMPTION = 'current_consumption' +ATTR_VOLTAGE = 'voltage' + CONF_LOCAL_KEY = "local_key" CONF_PROTOCOL_VERSION = "protocol_version" CONF_CURRENT = "current" @@ -9,4 +12,5 @@ CONF_VOLTAGE = "voltage" DOMAIN = "localtuya" -DEVICE_TYPE_POWER_OUTLET = "Power Outlet" +# Platforms in this list must support config flows +PLATFORMS = ["switch"] diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index 286cfad..88d9dad 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -28,161 +28,131 @@ import logging import voluptuous as vol from homeassistant.components.switch import SwitchEntity, PLATFORM_SCHEMA -from homeassistant.const import (CONF_HOST, CONF_ID, CONF_SWITCHES, CONF_DEVICE_ID, CONF_FRIENDLY_NAME, CONF_ICON, CONF_NAME) +from homeassistant.const import ( + CONF_HOST, + CONF_ID, + CONF_ENTITIES, + CONF_SWITCHES, + CONF_DEVICE_ID, + CONF_FRIENDLY_NAME, + CONF_ICON, + CONF_NAME, + CONF_PLATFORM, +) +from homeassistant.config_entries import SOURCE_IMPORT import homeassistant.helpers.config_validation as cv from time import time, sleep from threading import Lock from . import pytuya -from .const import CONF_LOCAL_KEY, CONF_PROTOCOL_VERSION, CONF_CURRENT, CONF_CURRENT_CONSUMPTION, CONF_VOLTAGE +from .const import ( + ATTR_CURRENT, + ATTR_CURRENT_CONSUMPTION, + ATTR_VOLTAGE, + CONF_LOCAL_KEY, + CONF_PROTOCOL_VERSION, + CONF_CURRENT, + CONF_CURRENT_CONSUMPTION, + CONF_VOLTAGE, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pytuya==7.0.9'] +REQUIREMENTS = ["pytuya==7.0.9"] -DEFAULT_ID = '1' +DEFAULT_ID = "1" DEFAULT_PROTOCOL_VERSION = 3.3 -ATTR_CURRENT = 'current' -ATTR_CURRENT_CONSUMPTION = 'current_consumption' -ATTR_VOLTAGE = 'voltage' +SWITCH_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_FRIENDLY_NAME): cv.string, + vol.Optional(CONF_CURRENT, default="-1"): cv.string, + vol.Optional(CONF_CURRENT_CONSUMPTION, default="-1"): cv.string, + vol.Optional(CONF_VOLTAGE, default="-1"): cv.string, + } +) -SWITCH_SCHEMA = vol.Schema({ - vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FRIENDLY_NAME): cv.string, - vol.Optional(CONF_CURRENT, default='-1'): cv.string, - vol.Optional(CONF_CURRENT_CONSUMPTION, default='-1'): cv.string, - vol.Optional(CONF_VOLTAGE, default='-1'): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_ICON): cv.icon, + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(CONF_LOCAL_KEY): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_FRIENDLY_NAME): cv.string, + vol.Required( + CONF_PROTOCOL_VERSION, default=DEFAULT_PROTOCOL_VERSION + ): vol.Coerce(float), + vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, + vol.Optional(CONF_CURRENT, default="-1"): cv.string, + vol.Optional(CONF_CURRENT_CONSUMPTION, default="-1"): cv.string, + vol.Optional(CONF_VOLTAGE, default="-1"): cv.string, + vol.Optional(CONF_SWITCHES, default={}): vol.Schema({cv.slug: SWITCH_SCHEMA}), + } +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ICON): cv.icon, - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_LOCAL_KEY): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FRIENDLY_NAME): cv.string, - vol.Required(CONF_PROTOCOL_VERSION, default=DEFAULT_PROTOCOL_VERSION): vol.Coerce(float), - vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, - vol.Optional(CONF_CURRENT, default='-1'): cv.string, - vol.Optional(CONF_CURRENT_CONSUMPTION, default='-1'): cv.string, - vol.Optional(CONF_VOLTAGE, default='-1'): cv.string, - vol.Optional(CONF_SWITCHES, default={}): - vol.Schema({cv.slug: SWITCH_SCHEMA}), -}) +DPS_FIELDS = [ + vol.Required(CONF_CURRENT), + vol.Required(CONF_CURRENT_CONSUMPTION), + vol.Required(CONF_VOLTAGE), +] async def async_setup_entry(hass, config_entry, async_add_entities): """Setup a Tuya switch based on a config entry.""" + switches_to_setup = [ + entity + for entity in config_entry.data[CONF_ENTITIES] + if entity[CONF_PLATFORM] == "switch" + ] + if not switches_to_setup: + return + switches = [] pytuyadevice = pytuya.OutletDevice( config_entry.data[CONF_DEVICE_ID], config_entry.data[CONF_HOST], - config_entry.data[CONF_LOCAL_KEY]) + config_entry.data[CONF_LOCAL_KEY], + ) pytuyadevice.set_version(float(config_entry.data[CONF_PROTOCOL_VERSION])) - dps = {} - - for device_config in config_entry.data[CONF_SWITCHES]: - dps[str(device_config[CONF_ID])] = None - if device_config[CONF_CURRENT] != 0: - dps[str(device_config[CONF_CURRENT])] = None - if device_config[CONF_CURRENT_CONSUMPTION] != 0: - dps[str(device_config[CONF_CURRENT_CONSUMPTION])] = None - if device_config[CONF_VOLTAGE] != 0: - dps[str(device_config[CONF_VOLTAGE])] = None + pytuyadevice.set_dpsUsed({}) + for device_config in switches_to_setup: switches.append( - TuyaDevice( - TuyaCache(pytuyadevice), - device_config[CONF_NAME], - device_config[CONF_FRIENDLY_NAME], - None, # Icon - str(device_config[CONF_ID]), - str(device_config[CONF_CURRENT]), - str(device_config[CONF_CURRENT_CONSUMPTION]), - str(device_config[CONF_VOLTAGE]) - ) + TuyaDevice( + TuyaCache(pytuyadevice), + device_config[CONF_FRIENDLY_NAME], + device_config[CONF_ID], + device_config[CONF_CURRENT], + device_config[CONF_CURRENT_CONSUMPTION], + device_config[CONF_VOLTAGE], + ) ) - pytuyadevice.set_dpsUsed(dps) - async_add_entities(switches, True) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up of the Tuya switch.""" - from . import pytuya - - devices = config.get(CONF_SWITCHES) - - switches = [] - pytuyadevice = pytuya.OutletDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) - pytuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION))) - - if len(devices) > 0: - for object_id, device_config in devices.items(): - dps = {} - dps[device_config.get(CONF_ID)]=None - if device_config.get(CONF_CURRENT) != '-1': - dps[device_config.get(CONF_CURRENT)]=None - if device_config.get(CONF_CURRENT_CONSUMPTION) != '-1': - dps[device_config.get(CONF_CURRENT_CONSUMPTION)]=None - if device_config.get(CONF_VOLTAGE) != '-1': - dps[device_config.get(CONF_VOLTAGE)]=None - pytuyadevice.set_dpsUsed(dps) - - outlet_device = TuyaCache(pytuyadevice) - switches.append( - TuyaDevice( - outlet_device, - device_config.get(CONF_NAME), - device_config.get(CONF_FRIENDLY_NAME, object_id), - device_config.get(CONF_ICON), - device_config.get(CONF_ID), - device_config.get(CONF_CURRENT), - device_config.get(CONF_CURRENT_CONSUMPTION), - device_config.get(CONF_VOLTAGE) - ) - ) - - print('Setup localtuya subswitch [{}] with device ID [{}] '.format(device_config.get(CONF_FRIENDLY_NAME, object_id), device_config.get(CONF_ID))) - _LOGGER.info("Setup localtuya subswitch %s with device ID %s ", device_config.get(CONF_FRIENDLY_NAME, object_id), device_config.get(CONF_ID) ) - else: - dps = {} - dps[config.get(CONF_ID)]=None - if config.get(CONF_CURRENT) != '-1': - dps[config.get(CONF_CURRENT)]=None - if config.get(CONF_CURRENT_CONSUMPTION) != '-1': - dps[config.get(CONF_CURRENT_CONSUMPTION)]=None - if config.get(CONF_VOLTAGE) != '-1': - dps[config.get(CONF_VOLTAGE)]=None - pytuyadevice.set_dpsUsed(dps) - outlet_device = TuyaCache(pytuyadevice) - switches.append( - TuyaDevice( - outlet_device, - config.get(CONF_NAME), - config.get(CONF_FRIENDLY_NAME), - config.get(CONF_ICON), - config.get(CONF_ID), - config.get(CONF_CURRENT), - config.get(CONF_CURRENT_CONSUMPTION), - config.get(CONF_VOLTAGE) - ) + config[CONF_PLATFORM] = "switch" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) + ) - print('Setup localtuya switch [{}] with device ID [{}] '.format(config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID))) - _LOGGER.info("Setup localtuya switch %s with device ID %s ", config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID) ) + return True - add_devices(switches, True) class TuyaCache: """Cache wrapper for pytuya.OutletDevice""" def __init__(self, device): """Initialize the cache.""" - self._cached_status = '' + self._cached_status = "" self._cached_status_time = 0 self._device = device self._lock = Lock() @@ -198,26 +168,37 @@ class TuyaCache: status = self._device.status() return status except Exception: - print('Failed to update status of device [{}]'.format(self._device.address)) + print( + "Failed to update status of device [{}]".format( + self._device.address + ) + ) sleep(1.0) - if i+1 == 3: - _LOGGER.error("Failed to update status of device %s", self._device.address ) -# return None + if i + 1 == 3: + _LOGGER.error( + "Failed to update status of device %s", self._device.address + ) + # return None raise ConnectionError("Failed to update status .") def set_status(self, state, switchid): """Change the Tuya switch status and clear the cache.""" - self._cached_status = '' + self._cached_status = "" self._cached_status_time = 0 for i in range(5): try: return self._device.set_status(state, switchid) except Exception: - print('Failed to set status of device [{}]'.format(self._device.address)) - if i+1 == 3: - _LOGGER.error("Failed to set status of device %s", self._device.address ) + print( + "Failed to set status of device [{}]".format(self._device.address) + ) + if i + 1 == 3: + _LOGGER.error( + "Failed to set status of device %s", self._device.address + ) return -# raise ConnectionError("Failed to set status.") + + # raise ConnectionError("Failed to set status.") def status(self): """Get state of Tuya switch and cache the results.""" @@ -236,19 +217,30 @@ class TuyaCache: class TuyaDevice(SwitchEntity): """Representation of a Tuya switch.""" - def __init__(self, device, name, friendly_name, icon, switchid, attr_current, attr_consumption, attr_voltage): + def __init__( + self, + device, + friendly_name, + switchid, + attr_current, + attr_consumption, + attr_voltage, + ): """Initialize the Tuya switch.""" self._device = device self._name = friendly_name self._available = False - self._icon = icon self._switch_id = switchid self._attr_current = attr_current self._attr_consumption = attr_consumption self._attr_voltage = attr_voltage self._status = None self._state = None - print('Initialized tuya switch [{}] with switch status [{}] and state [{}]'.format(self._name, self._status, self._state)) + print( + "Initialized tuya switch [{}] with switch status [{}] and state [{}]".format( + self._name, self._status, self._state + ) + ) @property def name(self): @@ -274,22 +266,21 @@ class TuyaDevice(SwitchEntity): def device_state_attributes(self): attrs = {} try: - attrs[ATTR_CURRENT] = "{}".format(self._status['dps'][self._attr_current]) - attrs[ATTR_CURRENT_CONSUMPTION] = "{}".format(self._status['dps'][self._attr_consumption]/10) - attrs[ATTR_VOLTAGE] = "{}".format(self._status['dps'][self._attr_voltage]/10) -# print('attrs[ATTR_CURRENT]: [{}]'.format(attrs[ATTR_CURRENT])) -# print('attrs[ATTR_CURRENT_CONSUMPTION]: [{}]'.format(attrs[ATTR_CURRENT_CONSUMPTION])) -# print('attrs[ATTR_VOLTAGE]: [{}]'.format(attrs[ATTR_VOLTAGE])) + attrs[ATTR_CURRENT] = "{}".format(self._status["dps"][self._attr_current]) + attrs[ATTR_CURRENT_CONSUMPTION] = "{}".format( + self._status["dps"][self._attr_consumption] / 10 + ) + attrs[ATTR_VOLTAGE] = "{}".format( + self._status["dps"][self._attr_voltage] / 10 + ) + # print('attrs[ATTR_CURRENT]: [{}]'.format(attrs[ATTR_CURRENT])) + # print('attrs[ATTR_CURRENT_CONSUMPTION]: [{}]'.format(attrs[ATTR_CURRENT_CONSUMPTION])) + # print('attrs[ATTR_VOLTAGE]: [{}]'.format(attrs[ATTR_VOLTAGE])) except KeyError: pass return attrs - @property - def icon(self): - """Return the icon.""" - return self._icon - def turn_on(self, **kwargs): """Turn Tuya switch on.""" self._device.set_status(True, self._switch_id) @@ -302,7 +293,7 @@ class TuyaDevice(SwitchEntity): """Get state of Tuya switch.""" try: self._status = self._device.status() - self._state = self._status['dps'][self._switch_id] + self._state = self._status["dps"][self._switch_id] except Exception: self._available = False else: diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 898b99d..6ed5cfc 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -1,39 +1,43 @@ { "config": { "abort": { - "already_configured": "Device has already been configured.", - "unsupported_device_type": "Unsupported device type!" + "already_configured": "Device has already been configured." }, "error": { - "cannot_connect": "Cannot connect to device. Verify that address is correct.", + "cannot_connect": "Cannot connect to device. Verify that address is correct and try again.", "invalid_auth": "Failed to authenticate with device. Verify that device id and local key are correct.", "unknown": "An unknown error occurred. See log for details.", - "switch_already_configured": "Switch with this ID has already been configured." + "entity_already_configured": "Entity with this ID has already been configured." }, "step": { "user": { "title": "Add Tuya device", - "description": "Fill in the basic details and pick device type. The name entered here will be used to identify the integration itself (as seen in the `Integrations` page). You will name each sub-device in the following steps.", + "description": "Fill in the basic details and pick device type. The name entered here will be used to identify the integration itself (as seen in the `Integrations` page). You will add entities and give them names in the following steps.", "data": { "name": "Name", "host": "Host", "device_id": "Device ID", "local_key": "Local key", - "protocol_version": "Protocol Version", - "device_type": "Device type" + "protocol_version": "Protocol Version" } }, - "power_outlet": { - "title": "Add subswitch", - "description": "You are about to add subswitch number `{number}`. If you want to add another, tick `Add another switch` before continuing.", + "pick_entity_type": { + "title": "Entity type selection", + "description": "Please pick the type of entity you want to add.", + "data": { + "platform_to_add": "Platform", + "no_additional_platforms": "Do not add any more entities" + } + }, + "add_entity": { + "title": "Add new entity", + "description": "Please fill out the details for an entity with type `{platform}`.", "data": { "id": "ID", - "name": "Name", "friendly_name": "Friendly name", "current": "Current", "current_consumption": "Current Consumption", - "voltage": "Voltage", - "add_another_switch": "Add another switch" + "voltage": "Voltage" } } } From 43a36f458cba8dfc1234b9ff5efe1106bc725a62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Tue, 8 Sep 2020 20:03:21 +0200 Subject: [PATCH 03/17] Remove pre-filled values from config_flow Also improve relative module import by not hardcoding package path. --- custom_components/localtuya/config_flow.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index 76e3060..6d814ef 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -31,10 +31,10 @@ NO_ADDITIONAL_PLATFORMS = "no_additional_platforms" USER_SCHEMA = vol.Schema( { - vol.Required(CONF_NAME, default="bla"): str, - vol.Required(CONF_HOST, default="10.0.10.110"): str, - vol.Required(CONF_DEVICE_ID, default="307001182462ab4de00b"): str, - vol.Required(CONF_LOCAL_KEY, default="28ac0651f2d187df"): str, + vol.Required(CONF_NAME): str, + vol.Required(CONF_HOST): str, + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_LOCAL_KEY): str, vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]), } ) @@ -192,10 +192,9 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=config[CONF_NAME], data=config) def _set_platform(self, platform): + integration_module = ".".join(__name__.split(".")[:-1]) self.platform = platform - self.platform_dps_fields = import_module( - f"homeassistant.components.localtuya.{platform}" - ).DPS_FIELDS + self.platform_dps_fields = import_module("." + platform, integration_module).DPS_FIELDS class CannotConnect(exceptions.HomeAssistantError): From aa1ed67f9438f546a196b82b0d5a409dd57496d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Wed, 9 Sep 2020 22:05:23 +0200 Subject: [PATCH 04/17] Make energy attributes in switch optional --- custom_components/localtuya/switch.py | 31 +++++++++++---------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index 88d9dad..772c126 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -94,10 +94,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) + DPS_FIELDS = [ - vol.Required(CONF_CURRENT), - vol.Required(CONF_CURRENT_CONSUMPTION), - vol.Required(CONF_VOLTAGE), + vol.Optional(CONF_CURRENT), + vol.Optional(CONF_CURRENT_CONSUMPTION), + vol.Optional(CONF_VOLTAGE), ] @@ -126,9 +127,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): TuyaCache(pytuyadevice), device_config[CONF_FRIENDLY_NAME], device_config[CONF_ID], - device_config[CONF_CURRENT], - device_config[CONF_CURRENT_CONSUMPTION], - device_config[CONF_VOLTAGE], + device_config.get(CONF_CURRENT), + device_config.get(CONF_CURRENT_CONSUMPTION), + device_config.get(CONF_VOLTAGE), ) ) @@ -265,20 +266,14 @@ class TuyaDevice(SwitchEntity): @property def device_state_attributes(self): attrs = {} - try: - attrs[ATTR_CURRENT] = "{}".format(self._status["dps"][self._attr_current]) - attrs[ATTR_CURRENT_CONSUMPTION] = "{}".format( + if self._attr_current: + attrs[ATTR_CURRENT] = self._status["dps"][self._attr_current] + if self._attr_consumption: + attrs[ATTR_CURRENT_CONSUMPTION] = ( self._status["dps"][self._attr_consumption] / 10 ) - attrs[ATTR_VOLTAGE] = "{}".format( - self._status["dps"][self._attr_voltage] / 10 - ) - # print('attrs[ATTR_CURRENT]: [{}]'.format(attrs[ATTR_CURRENT])) - # print('attrs[ATTR_CURRENT_CONSUMPTION]: [{}]'.format(attrs[ATTR_CURRENT_CONSUMPTION])) - # print('attrs[ATTR_VOLTAGE]: [{}]'.format(attrs[ATTR_VOLTAGE])) - - except KeyError: - pass + if self._attr_voltage: + attrs[ATTR_VOLTAGE] = self._status["dps"][self._attr_voltage] / 10 return attrs def turn_on(self, **kwargs): From 7018625f049be07a69aeb699596f465e45e9fd10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Wed, 9 Sep 2020 10:55:22 +0200 Subject: [PATCH 05/17] Add light support and clean up schemas --- custom_components/localtuya/__init__.py | 68 +++++++- custom_components/localtuya/config_flow.py | 6 +- custom_components/localtuya/const.py | 2 +- custom_components/localtuya/light.py | 185 +++++++++------------ custom_components/localtuya/switch.py | 65 ++------ 5 files changed, 156 insertions(+), 170 deletions(-) diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index f3c215a..ff07ef8 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -1,13 +1,71 @@ """The LocalTuya integration integration.""" -import asyncio - import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_ID, + CONF_ICON, + CONF_NAME, + CONF_FRIENDLY_NAME, + CONF_HOST, + CONF_PLATFORM, + CONF_ENTITIES, +) +import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, PLATFORMS +from . import pytuya +from .const import CONF_LOCAL_KEY, CONF_PROTOCOL_VERSION, DOMAIN, PLATFORMS + + +DEFAULT_ID = "1" +DEFAULT_PROTOCOL_VERSION = 3.3 + +BASE_PLATFORM_SCHEMA = { + vol.Optional(CONF_ICON): cv.icon, # Deprecated: not used + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(CONF_LOCAL_KEY): cv.string, + vol.Optional(CONF_NAME): cv.string, # Deprecated: not used + vol.Required(CONF_FRIENDLY_NAME): cv.string, + vol.Required(CONF_PROTOCOL_VERSION, default=DEFAULT_PROTOCOL_VERSION): vol.Coerce( + float + ), + vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, +} + + +def prepare_setup_entities(config_entry, platform): + """Prepare ro setup entities for a platform.""" + entities_to_setup = [ + entity + for entity in config_entry.data[CONF_ENTITIES] + if entity[CONF_PLATFORM] == platform + ] + if not entities_to_setup: + return None, None + + device = pytuya.BulbDevice( + config_entry.data[CONF_DEVICE_ID], + config_entry.data[CONF_HOST], + config_entry.data[CONF_LOCAL_KEY], + ) + device.set_version(float(config_entry.data[CONF_PROTOCOL_VERSION])) + device.set_dpsUsed({}) + return device, entities_to_setup + + +def import_from_yaml(hass, config, platform): + """Import configuration from YAML.""" + config[CONF_PLATFORM] = platform + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) + + return True async def async_setup(hass: HomeAssistant, config: dict): diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index 6d814ef..d8ba181 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -174,7 +174,7 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(user_input[CONF_DEVICE_ID]) self._set_platform(user_input[CONF_PLATFORM]) - if len(user_input[CONF_SWITCHES]) > 0: + if len(user_input.get(CONF_SWITCHES, [])) > 0: for switch_conf in user_input[CONF_SWITCHES].values(): self.entities.append(_convert_entity(switch_conf)) else: @@ -194,7 +194,9 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def _set_platform(self, platform): integration_module = ".".join(__name__.split(".")[:-1]) self.platform = platform - self.platform_dps_fields = import_module("." + platform, integration_module).DPS_FIELDS + self.platform_dps_fields = import_module( + "." + platform, integration_module + ).DPS_FIELDS class CannotConnect(exceptions.HomeAssistantError): diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 9361216..532f260 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -13,4 +13,4 @@ CONF_VOLTAGE = "voltage" DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["switch"] +PLATFORMS = ["light", "switch"] diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index 1193eb6..d6ffe99 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -12,12 +12,15 @@ light: friendly_name: This Light protocol_version: 3.3 """ -import voluptuous as vol -from homeassistant.const import (CONF_HOST, CONF_ID, CONF_SWITCHES, CONF_FRIENDLY_NAME, CONF_ICON, CONF_NAME) -import homeassistant.helpers.config_validation as cv +import socket +import logging from time import time, sleep from threading import Lock -import logging + +from homeassistant.const import ( + CONF_ID, + CONF_FRIENDLY_NAME, +) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -26,64 +29,53 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_COLOR_TEMP, LightEntity, - PLATFORM_SCHEMA + PLATFORM_SCHEMA, ) from homeassistant.util import color as colorutil -import socket -REQUIREMENTS = ['pytuya==7.0.9'] +from . import BASE_PLATFORM_SCHEMA, import_from_yaml, prepare_setup_entities + +_LOGGER = logging.getLogger(__name__) -CONF_DEVICE_ID = 'device_id' -CONF_LOCAL_KEY = 'local_key' -CONF_PROTOCOL_VERSION = 'protocol_version' -# IMPORTANT, id is used as key for state and turning on and off, 1 was fine switched apparently but my bulbs need 20, other feature attributes count up from this, e.g. 21 mode, 22 brightnes etc, see my pytuya modification. -DEFAULT_ID = '1' -DEFAULT_PROTOCOL_VERSION = 3.3 MIN_MIRED = 153 MAX_MIRED = 370 UPDATE_RETRY_LIMIT = 3 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ICON): cv.icon, - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_LOCAL_KEY): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FRIENDLY_NAME): cv.string, - vol.Required(CONF_PROTOCOL_VERSION, default=DEFAULT_PROTOCOL_VERSION): vol.Coerce(float), - vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, -}) -log = logging.getLogger(__name__) -log.setLevel(level=logging.DEBUG) # Debug hack! +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA) + +DPS_FIELDS = [] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Setup a Tuya switch based on a config entry.""" + device, entities_to_setup = prepare_setup_entities(config_entry, "light") + if not entities_to_setup: + return + + lights = [] + for device_config in entities_to_setup: + lights.append( + TuyaDevice( + TuyaCache(device), + device_config[CONF_FRIENDLY_NAME], + device_config[CONF_ID], + ) + ) + + async_add_entities(lights, True) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up of the Tuya switch.""" - from . import pytuya + return import_from_yaml(hass, config, "light") - lights = [] - pytuyadevice = pytuya.BulbDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) - pytuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION))) - - bulb_device = TuyaCache(pytuyadevice) - lights.append( - TuyaDevice( - bulb_device, - config.get(CONF_NAME), - config.get(CONF_FRIENDLY_NAME), - config.get(CONF_ICON), - config.get(CONF_ID) - ) - ) - - add_devices(lights) class TuyaCache: """Cache wrapper for pytuya.BulbDevice""" def __init__(self, device): """Initialize the cache.""" - self._cached_status = '' + self._cached_status = "" self._cached_status_time = 0 self._device = device self._lock = Lock() @@ -96,41 +88,31 @@ class TuyaCache: def __get_status(self, switchid): for _ in range(UPDATE_RETRY_LIMIT): try: - status = self._device.status()['dps'][switchid] - return status - except ConnectionError: + return self._device.status()["dps"][switchid] + except (ConnectionError, socket.timeout): pass - except socket.timeout: - pass - log.warn( - "Failed to get status after {} tries".format(UPDATE_RETRY_LIMIT)) + _LOGGER.warning("Failed to get status after %d tries", UPDATE_RETRY_LIMIT) def set_status(self, state, switchid): """Change the Tuya switch status and clear the cache.""" - self._cached_status = '' + self._cached_status = "" self._cached_status_time = 0 for _ in range(UPDATE_RETRY_LIMIT): try: return self._device.set_status(state, switchid) - except ConnectionError: + except (ConnectionError, socket.timeout): pass - except socket.timeout: - pass - log.warn( - "Failed to set status after {} tries".format(UPDATE_RETRY_LIMIT)) + _LOGGER.warning("Failed to set status after %d tries", UPDATE_RETRY_LIMIT) def status(self, switchid): """Get state of Tuya switch and cache the results.""" - self._lock.acquire() - try: + with self._lock: now = time() if not self._cached_status or now - self._cached_status_time > 30: sleep(0.5) self._cached_status = self.__get_status(switchid) self._cached_status_time = time() return self._cached_status - finally: - self._lock.release() def cached_status(self): return self._cached_status @@ -145,67 +127,52 @@ class TuyaCache: for _ in range(UPDATE_RETRY_LIMIT): try: return self._device.brightness() - except ConnectionError: + except (ConnectionError, socket.timeout): pass except KeyError: return "999" - except socket.timeout: - pass - log.warn( - "Failed to get brightness after {} tries".format(UPDATE_RETRY_LIMIT)) + _LOGGER.warning("Failed to get brightness after %d tries", UPDATE_RETRY_LIMIT) def color_temp(self): for _ in range(UPDATE_RETRY_LIMIT): try: return self._device.colourtemp() - except ConnectionError: + except (ConnectionError, socket.timeout): pass except KeyError: return "999" - except socket.timeout: - pass - log.warn( - "Failed to get color temp after {} tries".format(UPDATE_RETRY_LIMIT)) + _LOGGER.warning("Failed to get color temp after %d tries", UPDATE_RETRY_LIMIT) def set_brightness(self, brightness): for _ in range(UPDATE_RETRY_LIMIT): try: return self._device.set_brightness(brightness) - except ConnectionError: + except (ConnectionError, KeyError, socket.timeout): pass - except KeyError: - pass - except socket.timeout: - pass - log.warn( - "Failed to set brightness after {} tries".format(UPDATE_RETRY_LIMIT)) + _LOGGER.warning("Failed to set brightness after %d tries", UPDATE_RETRY_LIMIT) def set_color_temp(self, color_temp): for _ in range(UPDATE_RETRY_LIMIT): try: return self._device.set_colourtemp(color_temp) - except ConnectionError: + except (ConnectionError, KeyError, socket.timeout): pass - except KeyError: - pass - except socket.timeout: - pass - log.warn( - "Failed to set color temp after {} tries".format(UPDATE_RETRY_LIMIT)) + _LOGGER.warning("Failed to set color temp after %d tries", UPDATE_RETRY_LIMIT) def state(self): - self._device.state(); - + self._device.state() + def turn_on(self): - self._device.turn_on(); + self._device.turn_on() def turn_off(self): - self._device.turn_off(); + self._device.turn_off() + class TuyaDevice(LightEntity): """Representation of a Tuya switch.""" - def __init__(self, device, name, friendly_name, icon, bulbid): + def __init__(self, device, friendly_name, bulbid): """Initialize the Tuya switch.""" self._device = device self._available = False @@ -213,7 +180,6 @@ class TuyaDevice(LightEntity): self._state = False self._brightness = 127 self._color_temp = 127 - self._icon = icon self._bulb_id = bulbid @property @@ -236,11 +202,6 @@ class TuyaDevice(LightEntity): """Check if Tuya switch is on.""" return self._state - @property - def icon(self): - """Return the icon.""" - return self._icon - def update(self): """Get state of Tuya switch.""" try: @@ -254,12 +215,12 @@ class TuyaDevice(LightEntity): status = self._device.status(self._bulb_id) self._state = status try: - brightness = int(self._device.brightness()) - if brightness > 254: - brightness = 255 - if brightness < 25: - brightness = 25 - self._brightness = brightness + brightness = int(self._device.brightness()) + if brightness > 254: + brightness = 255 + if brightness < 25: + brightness = 25 + self._brightness = brightness except TypeError: pass self._color_temp = self._device.color_temp() @@ -269,16 +230,16 @@ class TuyaDevice(LightEntity): """Return the brightness of the light.""" return self._brightness -# @property -# def hs_color(self): -# """Return the hs_color of the light.""" -# return (self._device.color_hsv()[0],self._device.color_hsv()[1]) + # @property + # def hs_color(self): + # """Return the hs_color of the light.""" + # return (self._device.color_hsv()[0],self._device.color_hsv()[1]) @property def color_temp(self): """Return the color_temp of the light.""" try: - return int(MAX_MIRED - (((MAX_MIRED - MIN_MIRED) / 255) * self._color_temp)) + return int(MAX_MIRED - (((MAX_MIRED - MIN_MIRED) / 255) * self._color_temp)) except TypeError: pass @@ -294,8 +255,8 @@ class TuyaDevice(LightEntity): def turn_on(self, **kwargs): """Turn on or control the light.""" - log.debug("Turning on, state: " + str(self._device.cached_status())) - if not self._device.cached_status(): + _LOGGER.debug("Turning on, state: %s", self._device.cached_status()) + if not self._device.cached_status(): self._device.set_status(True, self._bulb_id) if ATTR_BRIGHTNESS in kwargs: converted_brightness = int(kwargs[ATTR_BRIGHTNESS]) @@ -305,7 +266,11 @@ class TuyaDevice(LightEntity): if ATTR_HS_COLOR in kwargs: raise ValueError(" TODO implement RGB from HS") if ATTR_COLOR_TEMP in kwargs: - color_temp = int(255 - (255 / (MAX_MIRED - MIN_MIRED)) * (int(kwargs[ATTR_COLOR_TEMP]) - MIN_MIRED)) + color_temp = int( + 255 + - (255 / (MAX_MIRED - MIN_MIRED)) + * (int(kwargs[ATTR_COLOR_TEMP]) - MIN_MIRED) + ) self._device.set_color_temp(color_temp) def turn_off(self, **kwargs): @@ -318,5 +283,5 @@ class TuyaDevice(LightEntity): supports = SUPPORT_BRIGHTNESS if self._device.color_temp() != "999": supports = supports | SUPPORT_COLOR - #supports = supports | SUPPORT_COLOR_TEMP + # supports = supports | SUPPORT_COLOR_TEMP return supports diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index 772c126..9ae0473 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -22,52 +22,42 @@ switch: sw02: name: usb_plug friendly_name: USB Plug - id: 7 + id: 7 """ import logging +from time import time, sleep +from threading import Lock + import voluptuous as vol from homeassistant.components.switch import SwitchEntity, PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_ID, - CONF_ENTITIES, CONF_SWITCHES, - CONF_DEVICE_ID, CONF_FRIENDLY_NAME, - CONF_ICON, CONF_NAME, - CONF_PLATFORM, ) -from homeassistant.config_entries import SOURCE_IMPORT import homeassistant.helpers.config_validation as cv -from time import time, sleep -from threading import Lock -from . import pytuya +from . import BASE_PLATFORM_SCHEMA, prepare_setup_entities, import_from_yaml from .const import ( ATTR_CURRENT, ATTR_CURRENT_CONSUMPTION, ATTR_VOLTAGE, - CONF_LOCAL_KEY, - CONF_PROTOCOL_VERSION, CONF_CURRENT, CONF_CURRENT_CONSUMPTION, CONF_VOLTAGE, - DOMAIN, ) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ["pytuya==7.0.9"] - DEFAULT_ID = "1" -DEFAULT_PROTOCOL_VERSION = 3.3 +# TODO: This will eventully merge with DPS_FIELDS SWITCH_SCHEMA = vol.Schema( { vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, - vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_NAME): cv.string, # Deprecated: not used vol.Required(CONF_FRIENDLY_NAME): cv.string, vol.Optional(CONF_CURRENT, default="-1"): cv.string, vol.Optional(CONF_CURRENT_CONSUMPTION, default="-1"): cv.string, @@ -75,18 +65,8 @@ SWITCH_SCHEMA = vol.Schema( } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA).extend( { - vol.Optional(CONF_ICON): cv.icon, - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_LOCAL_KEY): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FRIENDLY_NAME): cv.string, - vol.Required( - CONF_PROTOCOL_VERSION, default=DEFAULT_PROTOCOL_VERSION - ): vol.Coerce(float), - vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, vol.Optional(CONF_CURRENT, default="-1"): cv.string, vol.Optional(CONF_CURRENT_CONSUMPTION, default="-1"): cv.string, vol.Optional(CONF_VOLTAGE, default="-1"): cv.string, @@ -104,27 +84,15 @@ DPS_FIELDS = [ async def async_setup_entry(hass, config_entry, async_add_entities): """Setup a Tuya switch based on a config entry.""" - switches_to_setup = [ - entity - for entity in config_entry.data[CONF_ENTITIES] - if entity[CONF_PLATFORM] == "switch" - ] - if not switches_to_setup: + device, entities_to_setup = prepare_setup_entities(config_entry, "switch") + if not entities_to_setup: return switches = [] - pytuyadevice = pytuya.OutletDevice( - config_entry.data[CONF_DEVICE_ID], - config_entry.data[CONF_HOST], - config_entry.data[CONF_LOCAL_KEY], - ) - pytuyadevice.set_version(float(config_entry.data[CONF_PROTOCOL_VERSION])) - pytuyadevice.set_dpsUsed({}) - - for device_config in switches_to_setup: + for device_config in entities_to_setup: switches.append( TuyaDevice( - TuyaCache(pytuyadevice), + TuyaCache(device), device_config[CONF_FRIENDLY_NAME], device_config[CONF_ID], device_config.get(CONF_CURRENT), @@ -138,14 +106,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def setup_platform(hass, config, add_devices, discovery_info=None): """Set up of the Tuya switch.""" - config[CONF_PLATFORM] = "switch" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - - return True + return import_from_yaml(hass, config, "switch") class TuyaCache: From 7efb3024fbb0118fe4da410ef5778587ed2ba0fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Wed, 9 Sep 2020 12:52:46 +0200 Subject: [PATCH 06/17] Support arbitrary flow schemas for platforms --- custom_components/localtuya/config_flow.py | 46 ++++++++++++---------- custom_components/localtuya/light.py | 5 ++- custom_components/localtuya/switch.py | 14 ++++--- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index d8ba181..173cffd 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -44,22 +44,30 @@ PICK_ENTITY_SCHEMA = vol.Schema( ) -def platform_schema(dps, additional_fields): +def dps_string_list(dps_data): + """Return list of friendly DPS values.""" + return [f"{id} (value: {value})" for id, value in dps_data.items()] + + +def platform_schema(dps_strings, schema): """Generate input validation schema for a platform.""" - dps_list = vol.In([f"{id} (value: {value})" for id, value in dps.items()]) return vol.Schema( { - vol.Required(CONF_ID): dps_list, + vol.Required(CONF_ID): vol.In(dps_strings), vol.Required(CONF_FRIENDLY_NAME): str, } - ).extend({conf: dps_list for conf in additional_fields}) + ).extend(schema) -def strip_dps_values(user_input, fields): +def strip_dps_values(user_input, dps_strings): """Remove values and keep only index for DPS config items.""" - for field in [CONF_ID] + fields: - user_input[field] = user_input[field].split(" ")[0] - return user_input + stripped = {} + for field, value in user_input.items(): + if value in dps_strings: + stripped[field] = user_input[field].split(" ")[0] + else: + stripped[field] = user_input[field] + return stripped async def validate_input(hass: core.HomeAssistant, data): @@ -75,7 +83,7 @@ async def validate_input(hass: core.HomeAssistant, data): raise CannotConnect except ValueError: raise InvalidAuth - return data["dps"] + return dps_string_list(data["dps"]) class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -87,9 +95,9 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize a new LocaltuyaConfigFlow.""" self.basic_info = None - self.dps_data = None + self.dps_strings = [] self.platform = None - self.platform_dps_fields = None + self.platform_schema = None self.entities = [] async def async_step_user(self, user_input=None): @@ -101,7 +109,7 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: self.basic_info = user_input - self.dps_data = await validate_input(self.hass, user_input) + self.dps_strings = await validate_input(self.hass, user_input) return await self.async_step_pick_entity_type() except CannotConnect: errors["base"] = "cannot_connect" @@ -144,16 +152,14 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) if not already_configured: user_input[CONF_PLATFORM] = self.platform - self.entities.append( - strip_dps_values(user_input, self.platform_dps_fields) - ) + self.entities.append(strip_dps_values(user_input, self.dps_strings)) return await self.async_step_pick_entity_type() errors["base"] = "entity_already_configured" return self.async_show_form( step_id="add_entity", - data_schema=platform_schema(self.dps_data, self.platform_dps_fields), + data_schema=platform_schema(self.dps_strings, self.platform_schema), errors=errors, description_placeholders={"platform": self.platform}, ) @@ -167,12 +173,12 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_FRIENDLY_NAME: conf[CONF_FRIENDLY_NAME], CONF_PLATFORM: self.platform, } - for field in self.platform_dps_fields: + for field in self.platform_schema.keys(): converted[str(field)] = conf[field] return converted await self.async_set_unique_id(user_input[CONF_DEVICE_ID]) - self._set_platform(user_input[CONF_PLATFORM]) + self._set_platform(user_input[CONF_PLATFORM], []) if len(user_input.get(CONF_SWITCHES, [])) > 0: for switch_conf in user_input[CONF_SWITCHES].values(): @@ -194,9 +200,9 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def _set_platform(self, platform): integration_module = ".".join(__name__.split(".")[:-1]) self.platform = platform - self.platform_dps_fields = import_module( + self.platform_schema = import_module( "." + platform, integration_module - ).DPS_FIELDS + ).flow_schema(self.dps_strings) class CannotConnect(exceptions.HomeAssistantError): diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index d6ffe99..37c57b0 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -43,7 +43,10 @@ UPDATE_RETRY_LIMIT = 3 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA) -DPS_FIELDS = [] + +def flow_schema(dps): + """Return schema used in config flow.""" + return {} async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index 9ae0473..8b12b6c 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -53,7 +53,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_ID = "1" -# TODO: This will eventully merge with DPS_FIELDS +# TODO: This will eventully merge with flow_schema SWITCH_SCHEMA = vol.Schema( { vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, @@ -75,11 +75,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA).extend( ) -DPS_FIELDS = [ - vol.Optional(CONF_CURRENT), - vol.Optional(CONF_CURRENT_CONSUMPTION), - vol.Optional(CONF_VOLTAGE), -] +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Optional(CONF_CURRENT): vol.In(dps), + vol.Optional(CONF_CURRENT_CONSUMPTION): vol.In(dps), + vol.Optional(CONF_VOLTAGE): vol.In(dps), + } async def async_setup_entry(hass, config_entry, async_add_entities): From 11763d90c500984b1ca0901376a1bb7cbd7e2efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Wed, 9 Sep 2020 23:06:28 +0200 Subject: [PATCH 07/17] Use generic Device in config flow --- custom_components/localtuya/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index 173cffd..eca5e1b 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -72,7 +72,7 @@ def strip_dps_values(user_input, dps_strings): async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" - pytuyadevice = pytuya.OutletDevice( + pytuyadevice = pytuya.Device( data[CONF_DEVICE_ID], data[CONF_HOST], data[CONF_LOCAL_KEY] ) pytuyadevice.set_version(float(data[CONF_PROTOCOL_VERSION])) From ae64158da5b49232ce569c8d0ca4c95074d9ee1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 10 Sep 2020 10:42:44 +0200 Subject: [PATCH 08/17] Use correct device class from pytuya --- custom_components/localtuya/__init__.py | 4 ++-- custom_components/localtuya/light.py | 5 ++++- custom_components/localtuya/switch.py | 5 ++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index ff07ef8..022bffb 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -36,7 +36,7 @@ BASE_PLATFORM_SCHEMA = { } -def prepare_setup_entities(config_entry, platform): +def prepare_setup_entities(config_entry, platform, device_class): """Prepare ro setup entities for a platform.""" entities_to_setup = [ entity @@ -46,7 +46,7 @@ def prepare_setup_entities(config_entry, platform): if not entities_to_setup: return None, None - device = pytuya.BulbDevice( + device = device_class( config_entry.data[CONF_DEVICE_ID], config_entry.data[CONF_HOST], config_entry.data[CONF_LOCAL_KEY], diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index 37c57b0..7f68c51 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -34,6 +34,7 @@ from homeassistant.components.light import ( from homeassistant.util import color as colorutil from . import BASE_PLATFORM_SCHEMA, import_from_yaml, prepare_setup_entities +from .pytuya import BulbDevice _LOGGER = logging.getLogger(__name__) @@ -51,7 +52,9 @@ def flow_schema(dps): async def async_setup_entry(hass, config_entry, async_add_entities): """Setup a Tuya switch based on a config entry.""" - device, entities_to_setup = prepare_setup_entities(config_entry, "light") + device, entities_to_setup = prepare_setup_entities( + config_entry, "light", BulbDevice + ) if not entities_to_setup: return diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index 8b12b6c..9dd64da 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -48,6 +48,7 @@ from .const import ( CONF_CURRENT_CONSUMPTION, CONF_VOLTAGE, ) +from .pytuya import OutletDevice _LOGGER = logging.getLogger(__name__) @@ -86,7 +87,9 @@ def flow_schema(dps): async def async_setup_entry(hass, config_entry, async_add_entities): """Setup a Tuya switch based on a config entry.""" - device, entities_to_setup = prepare_setup_entities(config_entry, "switch") + device, entities_to_setup = prepare_setup_entities( + config_entry, "switch", OutletDevice + ) if not entities_to_setup: return From edc9debf839a6553cc2ad26d754006454314e63a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 10 Sep 2020 10:20:14 +0200 Subject: [PATCH 09/17] Add config flow support for cover --- custom_components/localtuya/const.py | 9 +- custom_components/localtuya/cover.py | 263 +++++++++--------- .../localtuya/pytuya/__init__.py | 4 +- .../localtuya/translations/en.json | 5 +- 4 files changed, 148 insertions(+), 133 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 532f260..9cc9623 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -6,11 +6,18 @@ ATTR_VOLTAGE = 'voltage' CONF_LOCAL_KEY = "local_key" CONF_PROTOCOL_VERSION = "protocol_version" + +# switch CONF_CURRENT = "current" CONF_CURRENT_CONSUMPTION = "current_consumption" CONF_VOLTAGE = "voltage" +# cover +CONF_OPEN_CMD = 'open_cmd' +CONF_CLOSE_CMD = 'close_cmd' +CONF_STOP_CMD = 'stop_cmd' + DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["light", "switch"] +PLATFORMS = ["cover", "light", "switch"] diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index 8cd76cf..8f4af68 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -19,7 +19,8 @@ cover: """ import logging -import requests +from time import time, sleep +from threading import Lock import voluptuous as vol @@ -31,84 +32,74 @@ from homeassistant.components.cover import ( SUPPORT_STOP, SUPPORT_SET_POSITION, ) - -"""from . import DATA_TUYA, TuyaDevice""" -from homeassistant.components.cover import CoverEntity, PLATFORM_SCHEMA -from homeassistant.const import (CONF_HOST, CONF_ID, CONF_FRIENDLY_NAME, CONF_ICON, CONF_NAME) +from homeassistant.const import ( + CONF_ID, + CONF_FRIENDLY_NAME, +) import homeassistant.helpers.config_validation as cv -from time import time, sleep -from threading import Lock + +from . import BASE_PLATFORM_SCHEMA, prepare_setup_entities, import_from_yaml +from .const import CONF_OPEN_CMD, CONF_CLOSE_CMD, CONF_STOP_CMD +from .pytuya import CoverDevice _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'localtuyacover' +DEFAULT_OPEN_CMD = "on" +DEFAULT_CLOSE_CMD = "off" +DEFAULT_STOP_CMD = "stop" -REQUIREMENTS = ['pytuya==7.0.9'] - -CONF_DEVICE_ID = 'device_id' -CONF_LOCAL_KEY = 'local_key' -CONF_PROTOCOL_VERSION = 'protocol_version' - -CONF_OPEN_CMD = 'open_cmd' -CONF_CLOSE_CMD = 'close_cmd' -CONF_STOP_CMD = 'stop_cmd' - -DEFAULT_ID = '1' -DEFAULT_PROTOCOL_VERSION = 3.3 -DEFAULT_OPEN_CMD = 'on' -DEFAULT_CLOSE_CMD = 'off' -DEFAULT_STOP_CMD = 'stop' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ICON): cv.icon, - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_LOCAL_KEY): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FRIENDLY_NAME): cv.string, - vol.Required(CONF_PROTOCOL_VERSION, default=DEFAULT_PROTOCOL_VERSION): vol.Coerce(float), - vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, - vol.Optional(CONF_OPEN_CMD, default=DEFAULT_OPEN_CMD): cv.string, - vol.Optional(CONF_CLOSE_CMD, default=DEFAULT_CLOSE_CMD): cv.string, - vol.Optional(CONF_STOP_CMD, default=DEFAULT_STOP_CMD): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA).extend( + { + vol.Optional(CONF_OPEN_CMD, default=DEFAULT_OPEN_CMD): cv.string, + vol.Optional(CONF_CLOSE_CMD, default=DEFAULT_CLOSE_CMD): cv.string, + vol.Optional(CONF_STOP_CMD, default=DEFAULT_STOP_CMD): cv.string, + } +) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tuya cover devices.""" - from . import pytuya +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Optional(CONF_OPEN_CMD, default=DEFAULT_OPEN_CMD): str, + vol.Optional(CONF_CLOSE_CMD, default=DEFAULT_CLOSE_CMD): str, + vol.Optional(CONF_STOP_CMD, default=DEFAULT_STOP_CMD): str, + } - #_LOGGER.info("running def setup_platform from cover.py") - #_LOGGER.info("conf_open_cmd is %s", config.get(CONF_OPEN_CMD)) - #_LOGGER.info("conf_close_cmd is %s", config.get(CONF_CLOSE_CMD)) - #_LOGGER.info("conf_STOP_cmd is %s", config.get(CONF_STOP_CMD)) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Setup a Tuya cover based on a config entry.""" + device, entities_to_setup = prepare_setup_entities( + config_entry, "cover", CoverDevice + ) + if not entities_to_setup: + return + + # TODO: keeping for now but should be removed + dps = {} + dps["101"] = None + dps["102"] = None covers = [] - pytuyadevice = pytuya.CoverEntity(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) - pytuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION))) - dps = {} - dps[config.get(CONF_ID)]=None - dps["101"]=None - dps["102"]=None - pytuyadevice.set_dpsUsed(dps) - - cover_device = TuyaCoverCache(pytuyadevice) - covers.append( + for device_config in entities_to_setup: + dps[device_config[CONF_ID]] = None + covers.append( TuyaDevice( - cover_device, - config.get(CONF_NAME), - config.get(CONF_FRIENDLY_NAME), - config.get(CONF_ICON), - config.get(CONF_ID), - config.get(CONF_OPEN_CMD), - config.get(CONF_CLOSE_CMD), - config.get(CONF_STOP_CMD), + TuyaCoverCache(device), + device_config[CONF_FRIENDLY_NAME], + device_config[CONF_ID], + device_config.get(CONF_OPEN_CMD), + device_config.get(CONF_CLOSE_CMD), + device_config.get(CONF_STOP_CMD), ) - ) - print('Setup localtuya cover [{}] with device ID [{}] '.format(config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID))) - _LOGGER.info("Setup localtuya cover %s with device ID=%s open_cmd=%s close_cmd=%s stop_cmd=%s", config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID), config.get(CONF_OPEN_CMD), config.get(CONF_CLOSE_CMD), config.get(CONF_STOP_CMD) ) + ) - add_entities(covers, True) + device.set_dpsUsed(dps) + async_add_entities(covers, True) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up of the Tuya cover.""" + return import_from_yaml(hass, config, "cover") class TuyaCoverCache: @@ -116,7 +107,7 @@ class TuyaCoverCache: def __init__(self, device): """Initialize the cache.""" - self._cached_status = '' + self._cached_status = "" self._cached_status_time = 0 self._device = device self._lock = Lock() @@ -127,38 +118,49 @@ class TuyaCoverCache: return self._device.id def __get_status(self): - #_LOGGER.info("running def __get_status from cover") + # _LOGGER.info("running def __get_status from cover") for i in range(5): try: status = self._device.status() return status except Exception: - print('Failed to update status of device [{}]'.format(self._device.address)) + print( + "Failed to update status of device [{}]".format( + self._device.address + ) + ) sleep(1.0) - if i+1 == 3: - _LOGGER.error("Failed to update status of device %s", self._device.address ) -# return None + if i + 1 == 3: + _LOGGER.error( + "Failed to update status of device %s", self._device.address + ) + # return None raise ConnectionError("Failed to update status .") def set_status(self, state, switchid): - #_LOGGER.info("running def set_status from cover") + # _LOGGER.info("running def set_status from cover") """Change the Tuya switch status and clear the cache.""" - self._cached_status = '' + self._cached_status = "" self._cached_status_time = 0 for i in range(5): try: - #_LOGGER.info("Running a try from def set_status from cover where state=%s and switchid=%s", state, switchid) + # _LOGGER.info("Running a try from def set_status from cover where state=%s and switchid=%s", state, switchid) return self._device.set_status(state, switchid) except Exception: - print('Failed to set status of device [{}]'.format(self._device.address)) - if i+1 == 3: - _LOGGER.error("Failed to set status of device %s", self._device.address ) + print( + "Failed to set status of device [{}]".format(self._device.address) + ) + if i + 1 == 3: + _LOGGER.error( + "Failed to set status of device %s", self._device.address + ) return -# raise ConnectionError("Failed to set status.") + + # raise ConnectionError("Failed to set status.") def status(self): """Get state of Tuya switch and cache the results.""" - #_LOGGER.info("running def status(self) from cover") + # _LOGGER.info("running def status(self) from cover") self._lock.acquire() try: now = time() @@ -170,24 +172,26 @@ class TuyaCoverCache: finally: self._lock.release() + class TuyaDevice(CoverEntity): """Tuya cover devices.""" - def __init__(self, device, name, friendly_name, icon, switchid, open_cmd, close_cmd, stop_cmd): + def __init__(self, device, friendly_name, switchid, open_cmd, close_cmd, stop_cmd): self._device = device self._available = False self._name = friendly_name - self._friendly_name = friendly_name - self._icon = icon self._switch_id = switchid - self._status = self._device.status() - self._state = self._status['dps'][self._switch_id] + self._status = None + self._state = None self._position = 50 self._open_cmd = open_cmd self._close_cmd = close_cmd self._stop_cmd = stop_cmd - _LOGGER.info("running def __init__ of TuyaDevice(CoverEntity) from cover.py with self=%s device=%s name=%s friendly_name=%s icon=%s switchid=%s open_cmd=%s close_cmd=%s stop_cmd=%s", self, device, name, friendly_name, icon, switchid, open_cmd, close_cmd, stop_cmd) - print('Initialized tuya cover [{}] with switch status [{}] and state [{}]'.format(self._name, self._status, self._state)) + print( + "Initialized tuya cover [{}] with switch status [{}] and state [{}]".format( + self._name, self._status, self._state + ) + ) @property def name(self): @@ -222,105 +226,106 @@ class TuyaDevice(CoverEntity): @property def supported_features(self): """Flag supported features.""" - supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION + supported_features = ( + SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION + ) return supported_features - @property - def icon(self): - """Return the icon.""" - return self._icon - @property def current_cover_position(self): - #self.update() - #state = self._state -# _LOGGER.info("curr_pos() : %i", self._position) - #print('curr_pos() : state [{}]'.format(state)) + # self.update() + # state = self._state + # _LOGGER.info("curr_pos() : %i", self._position) + # print('curr_pos() : state [{}]'.format(state)) return self._position @property def is_opening(self): - #self.update() + # self.update() state = self._state - #print('is_opening() : state [{}]'.format(state)) - if state == 'on': + # print('is_opening() : state [{}]'.format(state)) + if state == "on": return True return False @property def is_closing(self): - #self.update() + # self.update() state = self._state - #print('is_closing() : state [{}]'.format(state)) - if state == 'off': + # print('is_closing() : state [{}]'.format(state)) + if state == "off": return True return False @property def is_closed(self): """Return if the cover is closed or not.""" - #_LOGGER.info("running is_closed from cover") - #self.update() + # _LOGGER.info("running is_closed from cover") + # self.update() state = self._state - #print('is_closed() : state [{}]'.format(state)) - if state == 'off': + # print('is_closed() : state [{}]'.format(state)) + if state == "off": return False - if state == 'on': + if state == "on": return True return None def set_cover_position(self, **kwargs): - #_LOGGER.info("running set_cover_position from cover") + # _LOGGER.info("running set_cover_position from cover") """Move the cover to a specific position.""" newpos = float(kwargs["position"]) -# _LOGGER.info("Set new pos: %f", newpos) + # _LOGGER.info("Set new pos: %f", newpos) currpos = self.current_cover_position posdiff = abs(newpos - currpos) -# 25 sec corrisponde alla chiusura/apertura completa + # 25 sec corrisponde alla chiusura/apertura completa mydelay = posdiff / 2.0 if newpos > currpos: -# _LOGGER.info("Opening to %f: delay %f", newpos, mydelay ) + # _LOGGER.info("Opening to %f: delay %f", newpos, mydelay ) self.open_cover() else: -# _LOGGER.info("Closing to %f: delay %f", newpos, mydelay ) + # _LOGGER.info("Closing to %f: delay %f", newpos, mydelay ) self.close_cover() - sleep( mydelay ) + sleep(mydelay) self.stop_cover() self._position = 50 # newpos -# self._state = 'on' -# self._device._device.open_cover() + + # self._state = 'on' + # self._device._device.open_cover() def open_cover(self, **kwargs): """Open the cover.""" - #_LOGGER.info("running open_cover from cover") + # _LOGGER.info("running open_cover from cover") self._device.set_status(self._open_cmd, self._switch_id) -# self._state = 'on' -# self._device._device.open_cover() + + # self._state = 'on' + # self._device._device.open_cover() def close_cover(self, **kwargs): - #_LOGGER.info("running close_cover from cover") + # _LOGGER.info("running close_cover from cover") """Close cover.""" - #_LOGGER.info('about to set_status from cover of off, %s', self._switch_id) + # _LOGGER.info('about to set_status from cover of off, %s', self._switch_id) self._device.set_status(self._close_cmd, self._switch_id) -# self._state = 'off' -# self._device._device.close_cover() + + # self._state = 'off' + # self._device._device.close_cover() def stop_cover(self, **kwargs): - #_LOGGER.info("running stop_cover from cover") + # _LOGGER.info("running stop_cover from cover") """Stop the cover.""" self._device.set_status(self._stop_cmd, self._switch_id) -# self._state = 'stop' -# self._device._device.stop_cover() + + # self._state = 'stop' + # self._device._device.stop_cover() def update(self): """Get state of Tuya switch.""" - #_LOGGER.info("running update(self) from cover") + # _LOGGER.info("running update(self) from cover") try: self._status = self._device.status() - self._state = self._status['dps'][self._switch_id] - #print('update() : state [{}]'.format(self._state)) + self._state = self._status["dps"][self._switch_id] + # print('update() : state [{}]'.format(self._state)) except Exception: self._available = False else: diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 0ac26fb..278a74a 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -442,7 +442,7 @@ class FanDevice(Device): def __init__(self, dev_id, address, local_key=None): super(FanDevice, self).__init__(dev_id, address, local_key) -class CoverEntity(Device): +class CoverDevice(Device): DPS_INDEX_MOVE = '1' DPS_INDEX_BL = '101' @@ -460,7 +460,7 @@ class CoverEntity(Device): else: print('Using PyCrypto ', Crypto.version_info) print('Using PyCrypto from ', Crypto.__file__) - super(CoverEntity, self).__init__(dev_id, address, local_key) + super(CoverDevice, self).__init__(dev_id, address, local_key) def open_cover(self, switch=1): """Turn the device on""" diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 6ed5cfc..c0be82a 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -37,7 +37,10 @@ "friendly_name": "Friendly name", "current": "Current", "current_consumption": "Current Consumption", - "voltage": "Voltage" + "voltage": "Voltage", + "open_cmd": "Open Command", + "close_cmd": "Close Command", + "stop_cmd": "Stop Command" } } } From 5b84f05ef1d68a08fc70c3a0f39e7aca30f4879c Mon Sep 17 00:00:00 2001 From: rospogrigio <49229287+rospogrigio@users.noreply.github.com> Date: Thu, 10 Sep 2020 11:46:24 +0200 Subject: [PATCH 10/17] Update README.md --- README.md | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/README.md b/README.md index a436c23..52c2370 100644 --- a/README.md +++ b/README.md @@ -111,23 +111,8 @@ Alternatively, you can install localtuya through HACS by adding this repository. ``` 2020-09-04 02:08:26 DEBUG (SyncWorker_26) [custom_components.localtuya.pytuya] decrypted result='{"devId":"REDACTED","dps":{"1":"stop","2":100,"3":40,"5":false,"7":"closing","8":"cancel","9":0,"10":0}}' ``` - -5. Make any necessary edits to the python file(s) for your device(s). For example, if you are using a switch device, you may want to edit switch.py to account for the IDs/DPs that are applciable to your specific device. -``` - #### switch.py snippet - @property - def device_state_attributes(self): - attrs = {} - try: - attrs[ATTR_CURRENT] = "{}".format(self._status['dps']['104']) # Modify to match your device's DPs - attrs[ATTR_CURRENT_CONSUMPTION] = "{}".format(self._status['dps']['105']/10) # Modify to match your device's DPs - attrs[ATTR_VOLTAGE] = "{}".format(self._status['dps']['106']/10) # Modify to match your device's DPs - except KeyError: - pass - return attrs -``` -6. Add any applicable sensors, using the below configuration.yaml entry as a guide: +5. Add any applicable sensors, using the below configuration.yaml entry as a guide: ``` sensor: - platform: template From 783a556ff0323bb632d21050ef5e4fb5417ade89 Mon Sep 17 00:00:00 2001 From: rospogrigio <49229287+rospogrigio@users.noreply.github.com> Date: Thu, 10 Sep 2020 11:48:53 +0200 Subject: [PATCH 11/17] Updated pytuya reference to 7.1.0 --- info.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/info.md b/info.md index 7cf88c1..f128399 100644 --- a/info.md +++ b/info.md @@ -6,7 +6,7 @@ Local handling for Tuya Switches under Home-Assistant and Hassio, getting parameters from them (as Power Meters: Voltage, Current, Watt). Supports 3 types of switches: one-gang switches, two-gang switches and wifi plug (with additional USB plugs). -Also introduced handling for Tuya Covers and Lights, introducing pytuya library 7.0.9. +Also introduced handling for Tuya Covers and Lights, introducing pytuya library 7.1.0 that finally handles the 'json obj data unvalid' error correctly. Developed substantially by merging the codes of NameLessJedi, mileperhour and TradeFace (see Thanks paragraph). @@ -114,6 +114,9 @@ cover: RGB integration (for devices integrating both plug switch, power meter, and led light) Create a switch for cover backlight (dps 101): pytuya library already supports it + + climate (thermostats) devices handling + # Thanks to: From eca400f5810119925cde1b948e9b415c545d0e26 Mon Sep 17 00:00:00 2001 From: rospogrigio <49229287+rospogrigio@users.noreply.github.com> Date: Thu, 10 Sep 2020 11:49:52 +0200 Subject: [PATCH 12/17] Update info.md --- info.md | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/info.md b/info.md index f128399..0d20151 100644 --- a/info.md +++ b/info.md @@ -16,22 +16,7 @@ Developed substantially by merging the codes of NameLessJedi, mileperhour and Tr 2. Identify on your Home-Assistant logs (putting your logging into debug mode), the different attributes you want to handle by HA. - 3. Find in the switch.py file that part, and edit it for ID/DPS that is correct for your device. -``` - @property - def device_state_attributes(self): - attrs = {} - try: - attrs[ATTR_CURRENT] = "{}".format(self._status['dps']['104']) - attrs[ATTR_CURRENT_CONSUMPTION] = "{}".format(self._status['dps']['105']/10) - attrs[ATTR_VOLTAGE] = "{}".format(self._status['dps']['106']/10) - except KeyError: - pass - return attrs -``` - NOTE: Original data from the device for Voltage and Watt, includes the first decimal. So if the value is 2203, the correct value is 220,3V. By this reason, this values are divided by 10 ('/10' in the script). While Current is sent in mA (int number, no decimals), so it don't need any conversion factor to be added on the declaration. - - 4. Use this declaration on your configuration.yaml file (you need to get the 'device_id' and 'local_key' parameters for your device, as it can be obtained on other tutorials on the web: + 3. Use this declaration on your configuration.yaml file (you need to get the 'device_id' and 'local_key' parameters for your device, as it can be obtained on other tutorials on the web: ``` ##### FOR ONE-GANG SWITCHES ##### switch: @@ -71,7 +56,7 @@ switch: NOTE2: for each switch/subswitch both name and friendly_name must be specified: name will be used as the entity ID, while friendly_name will be used as the name in the frontend. - 5. Use this declaration on your configuration.yaml file, for stating sensors that handle its attributes: + 4. Use this declaration on your configuration.yaml file, for stating sensors that handle its attributes: ``` sensor: - platform: template @@ -89,11 +74,11 @@ switch: {{ states.switch.sw01.attributes.current_consumption }} unit_of_measurement: 'W' ``` - 6. If all gone OK (your device's parameters local_key and device_id are correct), your switch is working, so the sensors are working too. + 5. If all gone OK (your device's parameters local_key and device_id are correct), your switch is working, so the sensors are working too. NOTE: You can do as changes as you want in scripts ant/or yaml files. But: You can't declare your "custom_component" as "tuya", tuya is a forbidden word from 0.88 version or so. So if you declare a switch.tuya, the embedded (cloud based) Tuya component will be load instead custom_component one. - 7. If you are using a cover device, this is the configuration to be used (as explained in cover.py): + 6. If you are using a cover device, this is the configuration to be used (as explained in cover.py): ``` cover: - platform: localtuya From f9a4b4ad8a57c3ad439c84d8557ddf2db9a7818d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 10 Sep 2020 12:07:59 +0200 Subject: [PATCH 13/17] Add config flow support for fan --- custom_components/localtuya/const.py | 2 +- custom_components/localtuya/fan.py | 421 +++++++++++++-------------- 2 files changed, 206 insertions(+), 217 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 9cc9623..c348a42 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -20,4 +20,4 @@ CONF_STOP_CMD = 'stop_cmd' DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["cover", "light", "switch"] +PLATFORMS = ["cover", "fan", "light", "switch"] diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 3fbdf6c..ac0a8af 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -1,216 +1,205 @@ -""" -Simple platform to control LOCALLY Tuya cover devices. - -Sample config yaml - -fan: - - platform: localtuya - host: 192.168.0.123 - local_key: 1234567891234567 - device_id: 123456789123456789abcd - name: fan guests - friendly_name: fan guests - protocol_version: 3.3 - id: 1 - -""" -import logging -import requests - -import voluptuous as vol - -from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, - FanEntity, SUPPORT_SET_SPEED, - SUPPORT_OSCILLATE, SUPPORT_DIRECTION, PLATFORM_SCHEMA) - -from homeassistant.const import STATE_OFF -"""from . import DATA_TUYA, TuyaDevice""" -from homeassistant.const import (CONF_HOST, CONF_ID, CONF_FRIENDLY_NAME, CONF_ICON, CONF_NAME) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = 'localtuyafan' - -REQUIREMENTS = ['pytuya==7.0.9'] - -CONF_DEVICE_ID = 'device_id' -CONF_LOCAL_KEY = 'local_key' -CONF_PROTOCOL_VERSION = 'protocol_version' - -DEFAULT_ID = '1' -DEFAULT_PROTOCOL_VERSION = 3.3 - - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ICON): cv.icon, - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_LOCAL_KEY): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FRIENDLY_NAME): cv.string, - vol.Required(CONF_PROTOCOL_VERSION, default=DEFAULT_PROTOCOL_VERSION): vol.Coerce(float), - vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up of the Tuya switch.""" - from . import pytuya - fans = [] - localtuyadevice = pytuya.FanDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) - localtuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION))) - _LOGGER.debug("localtuya fan: setup_platform: %s", localtuyadevice) - - fan_device = localtuyadevice - fans.append( - TuyaDevice( - fan_device, - config.get(CONF_NAME), - config.get(CONF_FRIENDLY_NAME), - config.get(CONF_ICON), - config.get(CONF_ID), - ) - ) - _LOGGER.info("Setup localtuya fan %s with device ID %s ", config.get(CONF_FRIENDLY_NAME), config.get(CONF_DEVICE_ID) ) - - add_entities(fans, True) - -class TuyaDevice(FanEntity): - """A demonstration fan component.""" - - # def __init__(self, hass, name: str, supported_features: int) -> None: - def __init__(self, device, name, friendly_name, icon, switchid): - """Initialize the entity.""" - self._device = device - self._name = friendly_name - self._available = False - self._friendly_name = friendly_name - self._icon = icon - self._switch_id = switchid - self._status = self._device.status() - self._state = False - self._supported_features = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE - self._speed = STATE_OFF - self._oscillating = False - - @property - def oscillating(self): - """Return current oscillating status.""" - #if self._speed == STATE_OFF: - # return False - _LOGGER.debug("localtuya fan: oscillating = %s", self._oscillating) - return self._oscillating - - @property - def name(self) -> str: - """Get entity name.""" - return self._name - - @property - def is_on(self): - """Check if Tuya switch is on.""" - return self._state - - @property - def speed(self) -> str: - """Return the current speed.""" - return self._speed - - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - # return ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] - - def turn_on(self, speed: str = None, **kwargs) -> None: - """Turn on the entity.""" - _LOGGER.debug("localtuya fan: turn_on speed to: %s", speed) - # if speed is None: - # speed = SPEED_MEDIUM - # self.set_speed(speed) - self._device.turn_on() - if speed is not None: - self.set_speed(speed) - self.schedule_update_ha_state() - - def turn_off(self, **kwargs) -> None: - """Turn off the entity.""" - _LOGGER.debug("localtuya fan: turn_off") - self._device.set_status(False, '1') - self._state = False - self.schedule_update_ha_state() - - def set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - _LOGGER.debug("localtuya fan: set_speed to: %s", speed) - self._speed = speed - # self.schedule_update_ha_state() - if speed == STATE_OFF: - self._device.set_status(False, '1') - self._state = False - elif speed == SPEED_LOW: - self._device.set_value('2', '1') - elif speed == SPEED_MEDIUM: - self._device.set_value('2', '2') - elif speed == SPEED_HIGH: - self._device.set_value('2', '3') - self.schedule_update_ha_state() - - # def set_direction(self, direction: str) -> None: - # """Set the direction of the fan.""" - # self.direction = direction - # self.schedule_update_ha_state() - - def oscillate(self, oscillating: bool) -> None: - """Set oscillation.""" - self._oscillating = oscillating - self._device.set_value('8', oscillating) - self.schedule_update_ha_state() - - # @property - # def current_direction(self) -> str: - # """Fan direction.""" - # return self.direction - - @property - def unique_id(self): - """Return unique device identifier.""" - _LOGGER.debug("localtuya fan unique_id = %s", self._device) - return self._device.id - - @property - def available(self): - """Return if device is available or not.""" - return self._available - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._supported_features - - def update(self): - """Get state of Tuya switch.""" - success = False - for i in range(3): - if success is False: - try: - status = self._device.status() - _LOGGER.debug("localtuya fan: status = %s", status) - self._state = status['dps']['1'] - if status['dps']['1'] == False: - self._speed = STATE_OFF - elif int(status['dps']['2']) == 1: - self._speed = SPEED_LOW - elif int(status['dps']['2']) == 2: - self._speed = SPEED_MEDIUM - elif int(status['dps']['2']) == 3: - self._speed = SPEED_HIGH - # self._speed = status['dps']['2'] - self._oscillating = status['dps']['8'] - success = True - except ConnectionError: - if i+1 == 3: - success = False - raise ConnectionError("Failed to update status.") - self._available = success +""" +Simple platform to control LOCALLY Tuya cover devices. + +Sample config yaml + +fan: + - platform: localtuya + host: 192.168.0.123 + local_key: 1234567891234567 + device_id: 123456789123456789abcd + name: fan guests + friendly_name: fan guests + protocol_version: 3.3 + id: 1 + +""" +import logging + +import voluptuous as vol + +from homeassistant.components.fan import ( + SPEED_LOW, + SPEED_MEDIUM, + SPEED_HIGH, + FanEntity, + SUPPORT_SET_SPEED, + SUPPORT_OSCILLATE, + SUPPORT_DIRECTION, + PLATFORM_SCHEMA, +) +from homeassistant.const import CONF_ID, CONF_FRIENDLY_NAME, STATE_OFF +import homeassistant.helpers.config_validation as cv + +from . import BASE_PLATFORM_SCHEMA, prepare_setup_entities, import_from_yaml +from .pytuya import FanDevice + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA) + + +def flow_schema(dps): + """Return schema used in config flow.""" + return {} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Setup a Tuya fan based on a config entry.""" + device, entities_to_setup = prepare_setup_entities(config_entry, "fan", FanDevice) + if not entities_to_setup: + return + + lights = [] + for device_config in entities_to_setup: + lights.append( + TuyaDevice( + device, + device_config[CONF_FRIENDLY_NAME], + device_config[CONF_ID], + ) + ) + + async_add_entities(lights, True) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up of the Tuya fan.""" + return import_from_yaml(hass, config, "fan") + + +class TuyaDevice(FanEntity): + """A demonstration fan component.""" + + def __init__(self, device, friendly_name, switchid): + """Initialize the entity.""" + self._device = device + self._name = friendly_name + self._available = False + self._friendly_name = friendly_name + self._switch_id = switchid + self._status = self._device.status() + self._state = False + self._supported_features = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE + self._speed = STATE_OFF + self._oscillating = False + + @property + def oscillating(self): + """Return current oscillating status.""" + # if self._speed == STATE_OFF: + # return False + _LOGGER.debug("localtuya fan: oscillating = %s", self._oscillating) + return self._oscillating + + @property + def name(self) -> str: + """Get entity name.""" + return self._name + + @property + def is_on(self): + """Check if Tuya switch is on.""" + return self._state + + @property + def speed(self) -> str: + """Return the current speed.""" + return self._speed + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + # return ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] + + def turn_on(self, speed: str = None, **kwargs) -> None: + """Turn on the entity.""" + _LOGGER.debug("localtuya fan: turn_on speed to: %s", speed) + # if speed is None: + # speed = SPEED_MEDIUM + # self.set_speed(speed) + self._device.turn_on() + if speed is not None: + self.set_speed(speed) + self.schedule_update_ha_state() + + def turn_off(self, **kwargs) -> None: + """Turn off the entity.""" + _LOGGER.debug("localtuya fan: turn_off") + self._device.set_status(False, "1") + self._state = False + self.schedule_update_ha_state() + + def set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + _LOGGER.debug("localtuya fan: set_speed to: %s", speed) + self._speed = speed + # self.schedule_update_ha_state() + if speed == STATE_OFF: + self._device.set_status(False, "1") + self._state = False + elif speed == SPEED_LOW: + self._device.set_value("2", "1") + elif speed == SPEED_MEDIUM: + self._device.set_value("2", "2") + elif speed == SPEED_HIGH: + self._device.set_value("2", "3") + self.schedule_update_ha_state() + + # def set_direction(self, direction: str) -> None: + # """Set the direction of the fan.""" + # self.direction = direction + # self.schedule_update_ha_state() + + def oscillate(self, oscillating: bool) -> None: + """Set oscillation.""" + self._oscillating = oscillating + self._device.set_value("8", oscillating) + self.schedule_update_ha_state() + + # @property + # def current_direction(self) -> str: + # """Fan direction.""" + # return self.direction + + @property + def unique_id(self): + """Return unique device identifier.""" + return f"local_{self._device.id}_{self._switch_id}" + + @property + def available(self): + """Return if device is available or not.""" + return self._available + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._supported_features + + def update(self): + """Get state of Tuya switch.""" + success = False + for i in range(3): + if success is False: + try: + status = self._device.status() + _LOGGER.debug("localtuya fan: status = %s", status) + self._state = status["dps"]["1"] + if status["dps"]["1"] == False: + self._speed = STATE_OFF + elif int(status["dps"]["2"]) == 1: + self._speed = SPEED_LOW + elif int(status["dps"]["2"]) == 2: + self._speed = SPEED_MEDIUM + elif int(status["dps"]["2"]) == 3: + self._speed = SPEED_HIGH + # self._speed = status['dps']['2'] + self._oscillating = status["dps"]["8"] + success = True + except ConnectionError: + if i + 1 == 3: + success = False + raise ConnectionError("Failed to update status.") + self._available = success From 27edcacde24edc1aa5184ee28d6a096929a444e4 Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Sun, 13 Sep 2020 14:59:24 +0200 Subject: [PATCH 14/17] Fixed missing merging conflicts --- custom_components/localtuya/cover.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index a7045cf..cd2a845 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -76,13 +76,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # TODO: keeping for now but should be removed dps = {} -<<<<<<< HEAD dps[config.get(CONF_ID)]=None pytuyadevice.set_dpsUsed(dps) -======= - dps["101"] = None - dps["102"] = None ->>>>>>> edc9debf839a6553cc2ad26d754006454314e63a covers = [] for device_config in entities_to_setup: @@ -96,19 +91,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): device_config.get(CONF_CLOSE_CMD), device_config.get(CONF_STOP_CMD), ) -<<<<<<< HEAD - ) - print('Setup localtuya cover [{}] with device ID [{}] '.format(config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID))) - _LOGGER.info("Setup localtuya cover %s with device ID=%s", config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID) ) - _LOGGER.debug("Cover %s uses open_cmd=%s close_cmd=%s stop_cmd=%s", config.get(CONF_FRIENDLY_NAME), config.get(CONF_OPEN_CMD), config.get(CONF_CLOSE_CMD), config.get(CONF_STOP_CMD) ) -======= +# print('Setup localtuya cover [{}] with device ID [{}] '.format(config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID))) +# _LOGGER.info("Setup localtuya cover %s with device ID=%s", config.get(CONF_FRIENDLY_NAME), config.get(CONF_ID) ) +# _LOGGER.debug("Cover %s uses open_cmd=%s close_cmd=%s stop_cmd=%s", config.get(CONF_FRIENDLY_NAME), config.get(CONF_OPEN_CMD), config.get(CONF_CLOSE_CMD), config.get(CONF_STOP_CMD) ) ) device.set_dpsUsed(dps) async_add_entities(covers, True) ->>>>>>> edc9debf839a6553cc2ad26d754006454314e63a - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up of the Tuya cover.""" return import_from_yaml(hass, config, "cover") @@ -199,16 +189,11 @@ class TuyaDevice(CoverEntity): self._open_cmd = open_cmd self._close_cmd = close_cmd self._stop_cmd = stop_cmd -<<<<<<< HEAD - #_LOGGER.info("running def __init__ of TuyaDevice(CoverEntity) from cover.py with self=%s device=%s name=%s friendly_name=%s icon=%s switchid=%s open_cmd=%s close_cmd=%s stop_cmd=%s", self, device, name, friendly_name, icon, switchid, open_cmd, close_cmd, stop_cmd) - print('Initialized tuya cover [{}] with switch status [{}] and state [{}]'.format(self._name, self._status, self._state)) -======= print( "Initialized tuya cover [{}] with switch status [{}] and state [{}]".format( self._name, self._status, self._state ) ) ->>>>>>> edc9debf839a6553cc2ad26d754006454314e63a @property def name(self): From 50cbde330b7edc90c23e88e4153cc33dda47fb93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 10 Sep 2020 12:07:59 +0200 Subject: [PATCH 15/17] Fix fan platform after merge --- custom_components/localtuya/fan.py | 117 +++++++++++++---------------- 1 file changed, 53 insertions(+), 64 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 75e6cf6..c8bb9cb 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -15,78 +15,68 @@ fan: """ import logging -import requests import voluptuous as vol -from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, - FanEntity, SUPPORT_SET_SPEED, - SUPPORT_OSCILLATE, SUPPORT_DIRECTION, PLATFORM_SCHEMA) - -from homeassistant.const import STATE_OFF -"""from . import DATA_TUYA, TuyaDevice""" -from homeassistant.const import (CONF_HOST, CONF_ID, CONF_FRIENDLY_NAME, CONF_ICON, CONF_NAME) +from homeassistant.components.fan import ( + SPEED_LOW, + SPEED_MEDIUM, + SPEED_HIGH, + FanEntity, + SUPPORT_SET_SPEED, + SUPPORT_OSCILLATE, + SUPPORT_DIRECTION, + PLATFORM_SCHEMA, +) +from homeassistant.const import CONF_ID, CONF_FRIENDLY_NAME, STATE_OFF import homeassistant.helpers.config_validation as cv +from . import BASE_PLATFORM_SCHEMA, prepare_setup_entities, import_from_yaml +from .pytuya import FanDevice + _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'localtuyafan' - -REQUIREMENTS = ['pytuya>=7.1.0'] - -CONF_DEVICE_ID = 'device_id' -CONF_LOCAL_KEY = 'local_key' -CONF_PROTOCOL_VERSION = 'protocol_version' - -DEFAULT_ID = '1' -DEFAULT_PROTOCOL_VERSION = 3.3 +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BASE_PLATFORM_SCHEMA) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ICON): cv.icon, - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_LOCAL_KEY): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_FRIENDLY_NAME): cv.string, - vol.Required(CONF_PROTOCOL_VERSION, default=DEFAULT_PROTOCOL_VERSION): vol.Coerce(float), - vol.Optional(CONF_ID, default=DEFAULT_ID): cv.string, -}) +def flow_schema(dps): + """Return schema used in config flow.""" + return {} -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up of the Tuya switch.""" - from . import pytuya +async def async_setup_entry(hass, config_entry, async_add_entities): + """Setup a Tuya fan based on a config entry.""" + device, entities_to_setup = prepare_setup_entities(config_entry, "fan", FanDevice) + if not entities_to_setup: + return + fans = [] - localtuyadevice = pytuya.FanDevice(config.get(CONF_DEVICE_ID), config.get(CONF_HOST), config.get(CONF_LOCAL_KEY)) - localtuyadevice.set_version(float(config.get(CONF_PROTOCOL_VERSION))) - _LOGGER.debug("localtuya fan: setup_platform: %s", localtuyadevice) - - fan_device = localtuyadevice - fans.append( + for device_config in entities_to_setup: + fans.append( TuyaDevice( - fan_device, - config.get(CONF_NAME), - config.get(CONF_FRIENDLY_NAME), - config.get(CONF_ICON), - config.get(CONF_ID), + device, + device_config[CONF_FRIENDLY_NAME], + device_config[CONF_ID], ) - ) - _LOGGER.info("Setup localtuya fan %s with device ID %s ", config.get(CONF_FRIENDLY_NAME), config.get(CONF_DEVICE_ID) ) + ) + + async_add_entities(fans, True) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up of the Tuya fan.""" + return import_from_yaml(hass, config, "fan") - add_entities(fans, True) class TuyaDevice(FanEntity): """A demonstration fan component.""" - # def __init__(self, hass, name: str, supported_features: int) -> None: - def __init__(self, device, name, friendly_name, icon, switchid): + def __init__(self, device, friendly_name, switchid): """Initialize the entity.""" self._device = device self._name = friendly_name self._available = False self._friendly_name = friendly_name - self._icon = icon self._switch_id = switchid self._status = self._device.status() self._state = False @@ -97,7 +87,7 @@ class TuyaDevice(FanEntity): @property def oscillating(self): """Return current oscillating status.""" - #if self._speed == STATE_OFF: + # if self._speed == STATE_OFF: # return False _LOGGER.debug("localtuya fan: oscillating = %s", self._oscillating) return self._oscillating @@ -137,7 +127,7 @@ class TuyaDevice(FanEntity): def turn_off(self, **kwargs) -> None: """Turn off the entity.""" _LOGGER.debug("localtuya fan: turn_off") - self._device.set_status(False, '1') + self._device.set_status(False, "1") self._state = False self.schedule_update_ha_state() @@ -147,14 +137,14 @@ class TuyaDevice(FanEntity): self._speed = speed # self.schedule_update_ha_state() if speed == STATE_OFF: - self._device.set_status(False, '1') + self._device.set_status(False, "1") self._state = False elif speed == SPEED_LOW: - self._device.set_value('2', '1') + self._device.set_value("2", "1") elif speed == SPEED_MEDIUM: - self._device.set_value('2', '2') + self._device.set_value("2", "2") elif speed == SPEED_HIGH: - self._device.set_value('2', '3') + self._device.set_value("2", "3") self.schedule_update_ha_state() # def set_direction(self, direction: str) -> None: @@ -165,7 +155,7 @@ class TuyaDevice(FanEntity): def oscillate(self, oscillating: bool) -> None: """Set oscillation.""" self._oscillating = oscillating - self._device.set_value('8', oscillating) + self._device.set_value("8", oscillating) self.schedule_update_ha_state() # @property @@ -176,8 +166,7 @@ class TuyaDevice(FanEntity): @property def unique_id(self): """Return unique device identifier.""" - _LOGGER.debug("localtuya fan unique_id = %s", self._device) - return self._device.id + return f"local_{self._device.id}_{self._switch_id}" @property def available(self): @@ -197,20 +186,20 @@ class TuyaDevice(FanEntity): try: status = self._device.status() _LOGGER.debug("localtuya fan: status = %s", status) - self._state = status['dps']['1'] - if status['dps']['1'] == False: + self._state = status["dps"]["1"] + if status["dps"]["1"] == False: self._speed = STATE_OFF - elif int(status['dps']['2']) == 1: + elif int(status["dps"]["2"]) == 1: self._speed = SPEED_LOW - elif int(status['dps']['2']) == 2: + elif int(status["dps"]["2"]) == 2: self._speed = SPEED_MEDIUM - elif int(status['dps']['2']) == 3: + elif int(status["dps"]["2"]) == 3: self._speed = SPEED_HIGH # self._speed = status['dps']['2'] - self._oscillating = status['dps']['8'] + self._oscillating = status["dps"]["8"] success = True except ConnectionError: - if i+1 == 3: + if i + 1 == 3: success = False raise ConnectionError("Failed to update status.") self._available = success From 0e94b681db696a2deb5e376b7535ea802808c5b9 Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Mon, 14 Sep 2020 20:44:55 +0200 Subject: [PATCH 16/17] Fixed import from YAML in config_flow.py --- custom_components/localtuya/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index eca5e1b..737153c 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -178,7 +178,7 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return converted await self.async_set_unique_id(user_input[CONF_DEVICE_ID]) - self._set_platform(user_input[CONF_PLATFORM], []) + self._set_platform(user_input[CONF_PLATFORM]) if len(user_input.get(CONF_SWITCHES, [])) > 0: for switch_conf in user_input[CONF_SWITCHES].values(): From 285bffdc680baa24651104ba35492c471932fbe1 Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Mon, 14 Sep 2020 20:57:50 +0200 Subject: [PATCH 17/17] Fixed async_setup_entry in cover.py --- custom_components/localtuya/cover.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index cd2a845..abeb170 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -76,8 +76,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # TODO: keeping for now but should be removed dps = {} - dps[config.get(CONF_ID)]=None - pytuyadevice.set_dpsUsed(dps) covers = [] for device_config in entities_to_setup: