Merge branch 'master' into master
This commit is contained in:
@@ -53,6 +53,8 @@ class LocaltuyaBinarySensor(LocalTuyaEntity, BinarySensorEntity):
|
||||
|
||||
def status_updated(self):
|
||||
"""Device status was updated."""
|
||||
super().status_updated()
|
||||
|
||||
state = str(self.dps(self._dp_id)).lower()
|
||||
if state == self._config[CONF_STATE_ON].lower():
|
||||
self._is_on = True
|
||||
@@ -63,6 +65,11 @@ class LocaltuyaBinarySensor(LocalTuyaEntity, BinarySensorEntity):
|
||||
"State for entity %s did not match state patterns", self.entity_id
|
||||
)
|
||||
|
||||
# No need to restore state for a sensor
|
||||
async def restore_state_when_connected(self):
|
||||
"""Do nothing for a sensor."""
|
||||
return
|
||||
|
||||
|
||||
async_setup_entry = partial(
|
||||
async_setup_entry, DOMAIN, LocaltuyaBinarySensor, flow_schema
|
||||
|
@@ -13,6 +13,7 @@ from homeassistant.const import (
|
||||
CONF_ID,
|
||||
CONF_PLATFORM,
|
||||
CONF_SCAN_INTERVAL,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
@@ -31,6 +32,10 @@ from .const import (
|
||||
DATA_CLOUD,
|
||||
DOMAIN,
|
||||
TUYA_DEVICES,
|
||||
CONF_DEFAULT_VALUE,
|
||||
ATTR_STATE,
|
||||
CONF_RESTORE_ON_RECONNECT,
|
||||
CONF_RESET_DPIDS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -91,6 +96,8 @@ async def async_setup_entry(
|
||||
entity_config[CONF_ID],
|
||||
)
|
||||
)
|
||||
# Once the entities have been created, add to the TuyaDevice instance
|
||||
tuyainterface.add_entities(entities)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -135,13 +142,31 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
||||
self._connect_task = None
|
||||
self._disconnect_task = None
|
||||
self._unsub_interval = None
|
||||
self._entities = []
|
||||
self._local_key = self._dev_config_entry[CONF_LOCAL_KEY]
|
||||
self._default_reset_dpids = None
|
||||
if CONF_RESET_DPIDS in self._dev_config_entry:
|
||||
reset_ids_str = self._dev_config_entry[CONF_RESET_DPIDS].split(",")
|
||||
|
||||
self._default_reset_dpids = []
|
||||
for reset_id in reset_ids_str:
|
||||
self._default_reset_dpids.append(int(reset_id.strip()))
|
||||
|
||||
self.set_logger(_LOGGER, self._dev_config_entry[CONF_DEVICE_ID])
|
||||
|
||||
# This has to be done in case the device type is type_0d
|
||||
for entity in self._dev_config_entry[CONF_ENTITIES]:
|
||||
self.dps_to_request[entity[CONF_ID]] = None
|
||||
|
||||
def add_entities(self, entities):
|
||||
"""Set the entities associated with this device."""
|
||||
self._entities.extend(entities)
|
||||
|
||||
@property
|
||||
def is_connecting(self):
|
||||
"""Return whether device is currently connecting."""
|
||||
return self._connect_task is not None
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Return if connected to device."""
|
||||
@@ -165,13 +190,64 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
||||
self,
|
||||
)
|
||||
self._interface.add_dps_to_request(self.dps_to_request)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
self.exception(f"Connect to {self._dev_config_entry[CONF_HOST]} failed")
|
||||
if self._interface is not None:
|
||||
await self._interface.close()
|
||||
self._interface = None
|
||||
|
||||
self.debug("Retrieving initial state")
|
||||
status = await self._interface.status()
|
||||
if status is None:
|
||||
raise Exception("Failed to retrieve status")
|
||||
if self._interface is not None:
|
||||
try:
|
||||
self.debug("Retrieving initial state")
|
||||
status = await self._interface.status()
|
||||
if status is None:
|
||||
raise Exception("Failed to retrieve status")
|
||||
|
||||
self.status_updated(status)
|
||||
self._interface.start_heartbeat()
|
||||
self.status_updated(status)
|
||||
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
try:
|
||||
self.debug(
|
||||
"Initial state update failed, trying reset command "
|
||||
+ "for DP IDs: %s",
|
||||
self._default_reset_dpids,
|
||||
)
|
||||
await self._interface.reset(self._default_reset_dpids)
|
||||
|
||||
self.debug("Update completed, retrying initial state")
|
||||
status = await self._interface.status()
|
||||
if status is None or not status:
|
||||
raise Exception("Failed to retrieve status") from ex
|
||||
|
||||
self._interface.start_heartbeat()
|
||||
self.status_updated(status)
|
||||
|
||||
except UnicodeDecodeError as e: # pylint: disable=broad-except
|
||||
self.exception(
|
||||
f"Connect to {self._dev_config_entry[CONF_HOST]} failed: %s",
|
||||
type(e),
|
||||
)
|
||||
if self._interface is not None:
|
||||
await self._interface.close()
|
||||
self._interface = None
|
||||
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
self.exception(
|
||||
f"Connect to {self._dev_config_entry[CONF_HOST]} failed"
|
||||
)
|
||||
if "json.decode" in str(type(e)):
|
||||
await self.update_local_key()
|
||||
|
||||
if self._interface is not None:
|
||||
await self._interface.close()
|
||||
self._interface = None
|
||||
|
||||
if self._interface is not None:
|
||||
# Attempt to restore status for all entities that need to first set
|
||||
# the DPS value before the device will respond with status.
|
||||
for entity in self._entities:
|
||||
await entity.restore_state_when_connected()
|
||||
|
||||
def _new_entity_handler(entity_id):
|
||||
self.debug(
|
||||
@@ -195,22 +271,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
||||
self._async_refresh,
|
||||
timedelta(seconds=self._dev_config_entry[CONF_SCAN_INTERVAL]),
|
||||
)
|
||||
except UnicodeDecodeError as e: # pylint: disable=broad-except
|
||||
self.exception(
|
||||
f"Connect to {self._dev_config_entry[CONF_HOST]} failed: %s", type(e)
|
||||
)
|
||||
if self._interface is not None:
|
||||
await self._interface.close()
|
||||
self._interface = None
|
||||
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
self.exception(f"Connect to {self._dev_config_entry[CONF_HOST]} failed")
|
||||
if "json.decode" in str(type(e)):
|
||||
await self.update_local_key()
|
||||
|
||||
if self._interface is not None:
|
||||
await self._interface.close()
|
||||
self._interface = None
|
||||
self._connect_task = None
|
||||
|
||||
async def update_local_key(self):
|
||||
@@ -254,7 +315,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
||||
try:
|
||||
await self._interface.set_dp(state, dp_index)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
self.exception("Failed to set DP %d to %d", dp_index, state)
|
||||
self.exception("Failed to set DP %d to %s", dp_index, str(state))
|
||||
else:
|
||||
self.error(
|
||||
"Not connected to device %s", self._dev_config_entry[CONF_FRIENDLY_NAME]
|
||||
@@ -305,6 +366,17 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
|
||||
self._config = get_entity_config(config_entry, dp_id)
|
||||
self._dp_id = dp_id
|
||||
self._status = {}
|
||||
self._state = None
|
||||
self._last_state = None
|
||||
|
||||
# Default value is available to be provided by Platform entities if required
|
||||
self._default_value = self._config.get(CONF_DEFAULT_VALUE)
|
||||
|
||||
""" Restore on connect setting is available to be provided by Platform entities
|
||||
if required"""
|
||||
self._restore_on_reconnect = (
|
||||
self._config.get(CONF_RESTORE_ON_RECONNECT) or False
|
||||
)
|
||||
self.set_logger(logger, self._dev_config_entry[CONF_DEVICE_ID])
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
@@ -325,6 +397,8 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
|
||||
self._status = status.copy()
|
||||
if status:
|
||||
self.status_updated()
|
||||
|
||||
# Update HA
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
signal = f"localtuya_{self._dev_config_entry[CONF_DEVICE_ID]}"
|
||||
@@ -336,6 +410,22 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
|
||||
signal = f"localtuya_entity_{self._dev_config_entry[CONF_DEVICE_ID]}"
|
||||
async_dispatcher_send(self.hass, signal, self.entity_id)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return entity specific state attributes to be saved.
|
||||
|
||||
These attributes are then available for restore when the
|
||||
entity is restored at startup.
|
||||
"""
|
||||
attributes = {}
|
||||
if self._state is not None:
|
||||
attributes[ATTR_STATE] = self._state
|
||||
elif self._last_state is not None:
|
||||
attributes[ATTR_STATE] = self._last_state
|
||||
|
||||
self.debug("Entity %s - Additional attributes: %s", self.name, attributes)
|
||||
return attributes
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device information for the device registry."""
|
||||
@@ -408,9 +498,89 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
|
||||
|
||||
Override in subclasses and update entity specific state.
|
||||
"""
|
||||
state = self.dps(self._dp_id)
|
||||
self._state = state
|
||||
|
||||
# Keep record in last_state as long as not during connection/re-connection,
|
||||
# as last state will be used to restore the previous state
|
||||
if (state is not None) and (not self._device.is_connecting):
|
||||
self._last_state = state
|
||||
|
||||
def status_restored(self, stored_state):
|
||||
"""Device status was restored.
|
||||
|
||||
Override in subclasses and update entity specific state.
|
||||
"""
|
||||
raw_state = stored_state.attributes.get(ATTR_STATE)
|
||||
if raw_state is not None:
|
||||
self._last_state = raw_state
|
||||
self.debug(
|
||||
"Restoring state for entity: %s - state: %s",
|
||||
self.name,
|
||||
str(self._last_state),
|
||||
)
|
||||
|
||||
def default_value(self):
|
||||
"""Return default value of this entity.
|
||||
|
||||
Override in subclasses to specify the default value for the entity.
|
||||
"""
|
||||
# Check if default value has been set - if not, default to the entity defaults.
|
||||
if self._default_value is None:
|
||||
self._default_value = self.entity_default_value()
|
||||
|
||||
return self._default_value
|
||||
|
||||
def entity_default_value(self): # pylint: disable=no-self-use
|
||||
"""Return default value of the entity type.
|
||||
|
||||
Override in subclasses to specify the default value for the entity.
|
||||
"""
|
||||
return 0
|
||||
|
||||
@property
|
||||
def restore_on_reconnect(self):
|
||||
"""Return whether the last state should be restored on a reconnect.
|
||||
|
||||
Useful where the device loses settings if powered off
|
||||
"""
|
||||
return self._restore_on_reconnect
|
||||
|
||||
async def restore_state_when_connected(self):
|
||||
"""Restore if restore_on_reconnect is set, or if no status has been yet found.
|
||||
|
||||
Which indicates a DPS that needs to be set before it starts returning
|
||||
status.
|
||||
"""
|
||||
if not self.restore_on_reconnect and (str(self._dp_id) in self._status):
|
||||
self.debug(
|
||||
"Entity %s (DP %d) - Not restoring as restore on reconnect is \
|
||||
disabled for this entity and the entity has an initial status",
|
||||
self.name,
|
||||
self._dp_id,
|
||||
)
|
||||
return
|
||||
|
||||
self.debug("Attempting to restore state for entity: %s", self.name)
|
||||
# Attempt to restore the current state - in case reset.
|
||||
restore_state = self._state
|
||||
|
||||
# If no state stored in the entity currently, go from last saved state
|
||||
if (restore_state == STATE_UNKNOWN) | (restore_state is None):
|
||||
self.debug("No current state for entity")
|
||||
restore_state = self._last_state
|
||||
|
||||
# If no current or saved state, then use the default value
|
||||
if restore_state is None:
|
||||
self.debug("No last restored state - using default")
|
||||
restore_state = self.default_value()
|
||||
|
||||
self.debug(
|
||||
"Entity %s (DP %d) - Restoring state: %s",
|
||||
self.name,
|
||||
self._dp_id,
|
||||
str(restore_state),
|
||||
)
|
||||
|
||||
# Manually initialise
|
||||
await self._device.set_dp(restore_state, self._dp_id)
|
||||
|
@@ -38,12 +38,14 @@ from .const import (
|
||||
CONF_NO_CLOUD,
|
||||
CONF_PRODUCT_NAME,
|
||||
CONF_PROTOCOL_VERSION,
|
||||
CONF_RESET_DPIDS,
|
||||
CONF_SETUP_CLOUD,
|
||||
CONF_USER_ID,
|
||||
DATA_CLOUD,
|
||||
DATA_DISCOVERY,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
CONF_MANUAL_DPS,
|
||||
)
|
||||
from .discovery import discover
|
||||
|
||||
@@ -88,6 +90,8 @@ CONFIGURE_DEVICE_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_DEVICE_ID): str,
|
||||
vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]),
|
||||
vol.Optional(CONF_SCAN_INTERVAL): int,
|
||||
vol.Optional(CONF_MANUAL_DPS): str,
|
||||
vol.Optional(CONF_RESET_DPIDS): str,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -99,6 +103,8 @@ DEVICE_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_FRIENDLY_NAME): cv.string,
|
||||
vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]),
|
||||
vol.Optional(CONF_SCAN_INTERVAL): int,
|
||||
vol.Optional(CONF_MANUAL_DPS): cv.string,
|
||||
vol.Optional(CONF_RESET_DPIDS): str,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -140,6 +146,8 @@ def options_schema(entities):
|
||||
vol.Required(CONF_LOCAL_KEY): str,
|
||||
vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]),
|
||||
vol.Optional(CONF_SCAN_INTERVAL): int,
|
||||
vol.Optional(CONF_MANUAL_DPS): str,
|
||||
vol.Optional(CONF_RESET_DPIDS): str,
|
||||
vol.Required(
|
||||
CONF_ENTITIES, description={"suggested_value": entity_names}
|
||||
): cv.multi_select(entity_names),
|
||||
@@ -231,6 +239,8 @@ async def validate_input(hass: core.HomeAssistant, data):
|
||||
detected_dps = {}
|
||||
|
||||
interface = None
|
||||
|
||||
reset_ids = None
|
||||
try:
|
||||
interface = await pytuya.connect(
|
||||
data[CONF_HOST],
|
||||
@@ -239,7 +249,42 @@ async def validate_input(hass: core.HomeAssistant, data):
|
||||
float(data[CONF_PROTOCOL_VERSION]),
|
||||
)
|
||||
|
||||
detected_dps = await interface.detect_available_dps()
|
||||
try:
|
||||
detected_dps = await interface.detect_available_dps()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
try:
|
||||
_LOGGER.debug("Initial state update failed, trying reset command")
|
||||
if CONF_RESET_DPIDS in data:
|
||||
reset_ids_str = data[CONF_RESET_DPIDS].split(",")
|
||||
reset_ids = []
|
||||
for reset_id in reset_ids_str:
|
||||
reset_ids.append(int(reset_id.strip()))
|
||||
_LOGGER.debug(
|
||||
"Reset DPIDs configured: %s (%s)",
|
||||
data[CONF_RESET_DPIDS],
|
||||
reset_ids,
|
||||
)
|
||||
await interface.reset(reset_ids)
|
||||
detected_dps = await interface.detect_available_dps()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.debug("No DPS able to be detected")
|
||||
detected_dps = {}
|
||||
|
||||
# if manual DPs are set, merge these.
|
||||
_LOGGER.debug("Detected DPS: %s", detected_dps)
|
||||
if CONF_MANUAL_DPS in data:
|
||||
|
||||
manual_dps_list = [dps.strip() for dps in data[CONF_MANUAL_DPS].split(",")]
|
||||
_LOGGER.debug(
|
||||
"Manual DPS Setting: %s (%s)", data[CONF_MANUAL_DPS], manual_dps_list
|
||||
)
|
||||
# merge the lists
|
||||
for new_dps in manual_dps_list + (reset_ids or []):
|
||||
# If the DPS not in the detected dps list, then add with a
|
||||
# default value indicating that it has been manually added
|
||||
if new_dps not in detected_dps:
|
||||
detected_dps[new_dps] = -1
|
||||
|
||||
except (ConnectionRefusedError, ConnectionResetError) as ex:
|
||||
raise CannotConnect from ex
|
||||
except ValueError as ex:
|
||||
@@ -253,6 +298,8 @@ async def validate_input(hass: core.HomeAssistant, data):
|
||||
if not detected_dps:
|
||||
raise EmptyDpsList
|
||||
|
||||
_LOGGER.debug("Total DPS: %s", detected_dps)
|
||||
|
||||
return dps_string_list(detected_dps)
|
||||
|
||||
|
||||
|
@@ -35,11 +35,15 @@ CONF_PRODUCT_KEY = "product_key"
|
||||
CONF_PRODUCT_NAME = "product_name"
|
||||
CONF_USER_ID = "user_id"
|
||||
|
||||
|
||||
CONF_ACTION = "action"
|
||||
CONF_ADD_DEVICE = "add_device"
|
||||
CONF_EDIT_DEVICE = "edit_device"
|
||||
CONF_SETUP_CLOUD = "setup_cloud"
|
||||
CONF_NO_CLOUD = "no_cloud"
|
||||
CONF_MANUAL_DPS = "manual_dps_strings"
|
||||
CONF_DEFAULT_VALUE = "dps_default_value"
|
||||
CONF_RESET_DPIDS = "reset_dpids"
|
||||
|
||||
# light
|
||||
CONF_BRIGHTNESS_LOWER = "brightness_lower"
|
||||
@@ -114,3 +118,16 @@ CONF_FAULT_DP = "fault_dp"
|
||||
CONF_PAUSED_STATE = "paused_state"
|
||||
CONF_RETURN_MODE = "return_mode"
|
||||
CONF_STOP_STATUS = "stop_status"
|
||||
|
||||
# number
|
||||
CONF_MIN_VALUE = "min_value"
|
||||
CONF_MAX_VALUE = "max_value"
|
||||
CONF_STEPSIZE_VALUE = "step_size"
|
||||
|
||||
# select
|
||||
CONF_OPTIONS = "select_options"
|
||||
CONF_OPTIONS_FRIENDLY = "select_options_friendly"
|
||||
|
||||
# States
|
||||
ATTR_STATE = "raw_state"
|
||||
CONF_RESTORE_ON_RECONNECT = "restore_on_reconnect"
|
||||
|
@@ -228,5 +228,10 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity):
|
||||
# store the time of the last movement change
|
||||
self._timer_start = time.time()
|
||||
|
||||
# Keep record in last_state as long as not during connection/re-connection,
|
||||
# as last state will be used to restore the previous state
|
||||
if (self._state is not None) and (not self._device.is_connecting):
|
||||
self._last_state = self._state
|
||||
|
||||
|
||||
async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaCover, flow_schema)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "localtuya",
|
||||
"name": "LocalTuya integration",
|
||||
"version": "4.0.2",
|
||||
"version": "4.1.0",
|
||||
"documentation": "https://github.com/rospogrigio/localtuya/",
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
|
@@ -8,13 +8,19 @@ from homeassistant.const import CONF_DEVICE_CLASS, STATE_UNKNOWN
|
||||
|
||||
from .common import LocalTuyaEntity, async_setup_entry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .const import (
|
||||
CONF_MIN_VALUE,
|
||||
CONF_MAX_VALUE,
|
||||
CONF_DEFAULT_VALUE,
|
||||
CONF_RESTORE_ON_RECONNECT,
|
||||
CONF_STEPSIZE_VALUE,
|
||||
)
|
||||
|
||||
CONF_MIN_VALUE = "native_min_value"
|
||||
CONF_MAX_VALUE = "native_max_value"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_MIN = 0
|
||||
DEFAULT_MAX = 100000
|
||||
DEFAULT_STEP = 1.0
|
||||
|
||||
|
||||
def flow_schema(dps):
|
||||
@@ -28,6 +34,12 @@ def flow_schema(dps):
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=-1000000.0, max=1000000.0),
|
||||
),
|
||||
vol.Required(CONF_STEPSIZE_VALUE, default=DEFAULT_STEP): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=0.0, max=1000000.0),
|
||||
),
|
||||
vol.Optional(CONF_DEFAULT_VALUE): str,
|
||||
vol.Required(CONF_RESTORE_ON_RECONNECT): bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +61,18 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity):
|
||||
if CONF_MIN_VALUE in self._config:
|
||||
self._min_value = self._config.get(CONF_MIN_VALUE)
|
||||
|
||||
self._max_value = self._config.get(CONF_MAX_VALUE)
|
||||
self._max_value = DEFAULT_MAX
|
||||
if CONF_MAX_VALUE in self._config:
|
||||
self._max_value = self._config.get(CONF_MAX_VALUE)
|
||||
|
||||
self._step_size = DEFAULT_STEP
|
||||
if CONF_STEPSIZE_VALUE in self._config:
|
||||
self._step_size = self._config.get(CONF_STEPSIZE_VALUE)
|
||||
|
||||
# Override standard default value handling to cast to a float
|
||||
default_value = self._config.get(CONF_DEFAULT_VALUE)
|
||||
if default_value is not None:
|
||||
self._default_value = float(default_value)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
@@ -66,6 +89,11 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity):
|
||||
"""Return the maximum value."""
|
||||
return self._max_value
|
||||
|
||||
@property
|
||||
def native_step(self) -> float:
|
||||
"""Return the maximum value."""
|
||||
return self._step_size
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device."""
|
||||
@@ -75,10 +103,10 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity):
|
||||
"""Update the current value."""
|
||||
await self._device.set_dp(value, self._dp_id)
|
||||
|
||||
def status_updated(self):
|
||||
"""Device status was updated."""
|
||||
state = self.dps(self._dp_id)
|
||||
self._state = state
|
||||
# Default value is the minimum value
|
||||
def entity_default_value(self):
|
||||
"""Return the minimum value as the default for this entity type."""
|
||||
return self._min_value
|
||||
|
||||
|
||||
async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaNumber, flow_schema)
|
||||
|
@@ -62,6 +62,7 @@ TuyaMessage = namedtuple("TuyaMessage", "seqno cmd retcode payload crc")
|
||||
SET = "set"
|
||||
STATUS = "status"
|
||||
HEARTBEAT = "heartbeat"
|
||||
RESET = "reset"
|
||||
UPDATEDPS = "updatedps" # Request refresh of DPS
|
||||
|
||||
PROTOCOL_VERSION_BYTES_31 = b"3.1"
|
||||
@@ -96,6 +97,16 @@ PAYLOAD_DICT = {
|
||||
SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}},
|
||||
HEARTBEAT: {"hexByte": 0x09, "command": {}},
|
||||
UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}},
|
||||
RESET: {
|
||||
"hexByte": 0x12,
|
||||
"command": {
|
||||
"gwId": "",
|
||||
"devId": "",
|
||||
"uid": "",
|
||||
"t": "",
|
||||
"dpId": [18, 19, 20],
|
||||
},
|
||||
},
|
||||
},
|
||||
"type_0d": {
|
||||
STATUS: {"hexByte": 0x0D, "command": {"devId": "", "uid": "", "t": ""}},
|
||||
@@ -217,6 +228,7 @@ class MessageDispatcher(ContextualLogger):
|
||||
# Heartbeats always respond with sequence number 0, so they can't be waited for like
|
||||
# other messages. This is a hack to allow waiting for heartbeats.
|
||||
HEARTBEAT_SEQNO = -100
|
||||
RESET_SEQNO = -101
|
||||
|
||||
def __init__(self, dev_id, listener):
|
||||
"""Initialize a new MessageBuffer."""
|
||||
@@ -301,9 +313,19 @@ class MessageDispatcher(ContextualLogger):
|
||||
sem.release()
|
||||
elif msg.cmd == 0x12:
|
||||
self.debug("Got normal updatedps response")
|
||||
if self.RESET_SEQNO in self.listeners:
|
||||
sem = self.listeners[self.RESET_SEQNO]
|
||||
self.listeners[self.RESET_SEQNO] = msg
|
||||
sem.release()
|
||||
elif msg.cmd == 0x08:
|
||||
self.debug("Got status update")
|
||||
self.listener(msg)
|
||||
if self.RESET_SEQNO in self.listeners:
|
||||
self.debug("Got reset status update")
|
||||
sem = self.listeners[self.RESET_SEQNO]
|
||||
self.listeners[self.RESET_SEQNO] = msg
|
||||
sem.release()
|
||||
else:
|
||||
self.debug("Got status update")
|
||||
self.listener(msg)
|
||||
else:
|
||||
self.debug(
|
||||
"Got message type %d for unknown listener %d: %s",
|
||||
@@ -381,6 +403,11 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""Did connect to the device."""
|
||||
self.transport = transport
|
||||
self.on_connected.set_result(True)
|
||||
|
||||
def start_heartbeat(self):
|
||||
"""Start the heartbeat transmissions with the device."""
|
||||
|
||||
async def heartbeat_loop():
|
||||
"""Continuously send heart beat updates."""
|
||||
@@ -403,8 +430,6 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
||||
self.transport = None
|
||||
transport.close()
|
||||
|
||||
self.transport = transport
|
||||
self.on_connected.set_result(True)
|
||||
self.heartbeater = self.loop.create_task(heartbeat_loop())
|
||||
|
||||
def data_received(self, data):
|
||||
@@ -449,12 +474,13 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
||||
payload = self._generate_payload(command, dps)
|
||||
dev_type = self.dev_type
|
||||
|
||||
# Wait for special sequence number if heartbeat
|
||||
seqno = (
|
||||
MessageDispatcher.HEARTBEAT_SEQNO
|
||||
if command == HEARTBEAT
|
||||
else (self.seqno - 1)
|
||||
)
|
||||
# Wait for special sequence number if heartbeat or reset
|
||||
seqno = self.seqno - 1
|
||||
|
||||
if command == HEARTBEAT:
|
||||
seqno = MessageDispatcher.HEARTBEAT_SEQNO
|
||||
elif command == RESET:
|
||||
seqno = MessageDispatcher.RESET_SEQNO
|
||||
|
||||
self.transport.write(payload)
|
||||
msg = await self.dispatcher.wait_for(seqno)
|
||||
@@ -487,6 +513,15 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
||||
"""Send a heartbeat message."""
|
||||
return await self.exchange(HEARTBEAT)
|
||||
|
||||
async def reset(self, dpIds=None):
|
||||
"""Send a reset message (3.3 only)."""
|
||||
if self.version == 3.3:
|
||||
self.dev_type = "type_0a"
|
||||
self.debug("reset switching to dev_type %s", self.dev_type)
|
||||
return await self.exchange(RESET, dpIds)
|
||||
|
||||
return True
|
||||
|
||||
async def update_dps(self, dps=None):
|
||||
"""
|
||||
Request device to update index.
|
||||
|
@@ -4,14 +4,19 @@ from functools import partial
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.select import DOMAIN, SelectEntity
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, STATE_UNKNOWN
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
|
||||
from .common import LocalTuyaEntity, async_setup_entry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_OPTIONS = "select_options"
|
||||
CONF_OPTIONS_FRIENDLY = "select_options_friendly"
|
||||
from .const import (
|
||||
CONF_OPTIONS,
|
||||
CONF_OPTIONS_FRIENDLY,
|
||||
CONF_DEFAULT_VALUE,
|
||||
CONF_RESTORE_ON_RECONNECT,
|
||||
)
|
||||
|
||||
|
||||
def flow_schema(dps):
|
||||
@@ -19,9 +24,14 @@ def flow_schema(dps):
|
||||
return {
|
||||
vol.Required(CONF_OPTIONS): str,
|
||||
vol.Optional(CONF_OPTIONS_FRIENDLY): str,
|
||||
vol.Optional(CONF_DEFAULT_VALUE): str,
|
||||
vol.Required(CONF_RESTORE_ON_RECONNECT): bool,
|
||||
}
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LocaltuyaSelect(LocalTuyaEntity, SelectEntity):
|
||||
"""Representation of a Tuya Enumeration."""
|
||||
|
||||
@@ -92,9 +102,24 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity):
|
||||
|
||||
def status_updated(self):
|
||||
"""Device status was updated."""
|
||||
super().status_updated()
|
||||
|
||||
state = self.dps(self._dp_id)
|
||||
self._state_friendly = self._display_options[self._valid_options.index(state)]
|
||||
self._state = state
|
||||
|
||||
# Check that received status update for this entity.
|
||||
if state is not None:
|
||||
try:
|
||||
self._state_friendly = self._display_options[
|
||||
self._valid_options.index(state)
|
||||
]
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# Friendly value couldn't be mapped
|
||||
self._state_friendly = state
|
||||
|
||||
# Default value is the first option
|
||||
def entity_default_value(self):
|
||||
"""Return the first option as the default value for this entity type."""
|
||||
return self._valid_options[0]
|
||||
|
||||
|
||||
async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaSelect, flow_schema)
|
||||
|
@@ -66,5 +66,10 @@ class LocaltuyaSensor(LocalTuyaEntity):
|
||||
state = round(state * scale_factor, DEFAULT_PRECISION)
|
||||
self._state = state
|
||||
|
||||
# No need to restore state for a sensor
|
||||
async def restore_state_when_connected(self):
|
||||
"""Do nothing for a sensor."""
|
||||
return
|
||||
|
||||
|
||||
async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaSensor, flow_schema)
|
||||
|
@@ -121,7 +121,12 @@
|
||||
"preset_set": "Presets Set (optional)",
|
||||
"eco_dp": "Eco DP (optional)",
|
||||
"eco_value": "Eco value (optional)",
|
||||
"heuristic_action": "Enable heuristic action (optional)"
|
||||
"heuristic_action": "Enable heuristic action (optional)",
|
||||
"dps_default_value": "Default value when un-initialised (optional)",
|
||||
"restore_on_reconnect": "Restore the last set value in HomeAssistant after a lost connection",
|
||||
"min_value": "Minimum Value",
|
||||
"max_value": "Maximum Value",
|
||||
"step_size": "Minimum increment between numbers"
|
||||
}
|
||||
},
|
||||
"yaml_import": {
|
||||
|
@@ -10,9 +10,12 @@ from .const import (
|
||||
ATTR_CURRENT,
|
||||
ATTR_CURRENT_CONSUMPTION,
|
||||
ATTR_VOLTAGE,
|
||||
ATTR_STATE,
|
||||
CONF_CURRENT,
|
||||
CONF_CURRENT_CONSUMPTION,
|
||||
CONF_VOLTAGE,
|
||||
CONF_DEFAULT_VALUE,
|
||||
CONF_RESTORE_ON_RECONNECT,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -24,6 +27,8 @@ def flow_schema(dps):
|
||||
vol.Optional(CONF_CURRENT): vol.In(dps),
|
||||
vol.Optional(CONF_CURRENT_CONSUMPTION): vol.In(dps),
|
||||
vol.Optional(CONF_VOLTAGE): vol.In(dps),
|
||||
vol.Optional(CONF_DEFAULT_VALUE): str,
|
||||
vol.Required(CONF_RESTORE_ON_RECONNECT): bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +64,12 @@ class LocaltuyaSwitch(LocalTuyaEntity, SwitchEntity):
|
||||
)
|
||||
if self.has_config(CONF_VOLTAGE):
|
||||
attrs[ATTR_VOLTAGE] = self.dps(self._config[CONF_VOLTAGE]) / 10
|
||||
|
||||
# Store the state
|
||||
if self._state is not None:
|
||||
attrs[ATTR_STATE] = self._state
|
||||
elif self._last_state is not None:
|
||||
attrs[ATTR_STATE] = self._last_state
|
||||
return attrs
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
@@ -69,9 +80,10 @@ class LocaltuyaSwitch(LocalTuyaEntity, SwitchEntity):
|
||||
"""Turn Tuya switch off."""
|
||||
await self._device.set_dp(False, self._dp_id)
|
||||
|
||||
def status_updated(self):
|
||||
"""Device status was updated."""
|
||||
self._state = self.dps(self._dp_id)
|
||||
# Default value is the "OFF" state
|
||||
def entity_default_value(self):
|
||||
"""Return False as the defaualt value for this entity type."""
|
||||
return False
|
||||
|
||||
|
||||
async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaSwitch, flow_schema)
|
||||
|
@@ -96,7 +96,9 @@
|
||||
"local_key": "Local key",
|
||||
"protocol_version": "Protocol Version",
|
||||
"scan_interval": "Scan interval (seconds, only when not updating automatically)",
|
||||
"entities": "Entities (uncheck an entity to remove it)"
|
||||
"entities": "Entities (uncheck an entity to remove it)",
|
||||
"manual_dps_strings": "Manual DPS to add (separated by commas ',') - used when detection is not working (optional)",
|
||||
"reset_dpids": "DPIDs to send in RESET command (separated by commas ',')- Used when device does not respond to status requests after turning on (optional)"
|
||||
}
|
||||
},
|
||||
"pick_entity_type": {
|
||||
@@ -182,7 +184,12 @@
|
||||
"preset_set": "Presets Set (optional)",
|
||||
"eco_dp": "Eco DP (optional)",
|
||||
"eco_value": "Eco value (optional)",
|
||||
"heuristic_action": "Enable heuristic action (optional)"
|
||||
"heuristic_action": "Enable heuristic action (optional)",
|
||||
"dps_default_value": "Default value when un-initialised (optional)",
|
||||
"restore_on_reconnect": "Restore the last set value in HomeAssistant after a lost connection",
|
||||
"min_value": "Minimum Value",
|
||||
"max_value": "Maximum Value",
|
||||
"step_size": "Minimum increment between numbers"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user