Merge pull request #549 from vmartinv/update_interval
Periodically send update dps command
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""Code shared between all platforms."""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_ID,
|
||||
@@ -9,8 +10,10 @@ from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_ID,
|
||||
CONF_PLATFORM,
|
||||
CONF_SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
@@ -117,6 +120,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
||||
self._is_closing = False
|
||||
self._connect_task = None
|
||||
self._disconnect_task = None
|
||||
self._unsub_interval = None
|
||||
self.set_logger(_LOGGER, config_entry[CONF_DEVICE_ID])
|
||||
|
||||
# This has to be done in case the device type is type_0d
|
||||
@@ -166,6 +170,16 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
||||
self._disconnect_task = async_dispatcher_connect(
|
||||
self._hass, signal, _new_entity_handler
|
||||
)
|
||||
|
||||
if (
|
||||
CONF_SCAN_INTERVAL in self._config_entry
|
||||
and self._config_entry[CONF_SCAN_INTERVAL] > 0
|
||||
):
|
||||
self._unsub_interval = async_track_time_interval(
|
||||
self._hass,
|
||||
self._async_refresh,
|
||||
timedelta(seconds=self._config_entry[CONF_SCAN_INTERVAL]),
|
||||
)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
self.exception(f"Connect to {self._config_entry[CONF_HOST]} failed")
|
||||
if self._interface is not None:
|
||||
@@ -173,6 +187,10 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
||||
self._interface = None
|
||||
self._connect_task = None
|
||||
|
||||
async def _async_refresh(self, _now):
|
||||
if self._interface is not None:
|
||||
await self._interface.update_dps()
|
||||
|
||||
async def close(self):
|
||||
"""Close connection and stop re-connect loop."""
|
||||
self._is_closing = True
|
||||
@@ -223,7 +241,9 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
||||
"""Device disconnected."""
|
||||
signal = f"localtuya_{self._config_entry[CONF_DEVICE_ID]}"
|
||||
async_dispatcher_send(self._hass, signal, None)
|
||||
|
||||
if self._unsub_interval is not None:
|
||||
self._unsub_interval()
|
||||
self._unsub_interval = None
|
||||
self._interface = None
|
||||
self.debug("Disconnected - waiting for discovery broadcast")
|
||||
|
||||
@@ -253,12 +273,12 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
|
||||
|
||||
def _update_handler(status):
|
||||
"""Update entity state when status was updated."""
|
||||
if status is not None:
|
||||
self._status = status
|
||||
if status is None:
|
||||
status = {}
|
||||
if self._status != status:
|
||||
self._status = status.copy()
|
||||
if status:
|
||||
self.status_updated()
|
||||
else:
|
||||
self._status = {}
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
signal = f"localtuya_{self._config_entry.data[CONF_DEVICE_ID]}"
|
||||
|
@@ -14,6 +14,7 @@ from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_ID,
|
||||
CONF_PLATFORM,
|
||||
CONF_SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
|
||||
@@ -44,6 +45,7 @@ BASIC_INFO_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_HOST): str,
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -55,6 +57,7 @@ DEVICE_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_LOCAL_KEY): cv.string,
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -90,6 +93,7 @@ def options_schema(entities):
|
||||
vol.Required(CONF_HOST): str,
|
||||
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.Required(
|
||||
CONF_ENTITIES, description={"suggested_value": entity_names}
|
||||
): cv.multi_select(entity_names),
|
||||
|
@@ -21,6 +21,7 @@ Functions
|
||||
json = status() # returns json payload
|
||||
set_version(version) # 3.1 [default] or 3.3
|
||||
detect_available_dps() # returns a list of available dps provided by the device
|
||||
update_dps(dps) # sends update dps command
|
||||
add_dps_to_request(dp_index) # adds dp_index to the list of dps used by the
|
||||
# device (to be queried in the payload)
|
||||
set_dp(on, dp_index) # Set value of any dps index.
|
||||
@@ -61,6 +62,7 @@ TuyaMessage = namedtuple("TuyaMessage", "seqno cmd retcode payload crc")
|
||||
SET = "set"
|
||||
STATUS = "status"
|
||||
HEARTBEAT = "heartbeat"
|
||||
UPDATEDPS = "updatedps" # Request refresh of DPS
|
||||
|
||||
PROTOCOL_VERSION_BYTES_31 = b"3.1"
|
||||
PROTOCOL_VERSION_BYTES_33 = b"3.3"
|
||||
@@ -76,6 +78,9 @@ SUFFIX_VALUE = 0x0000AA55
|
||||
|
||||
HEARTBEAT_INTERVAL = 10
|
||||
|
||||
# DPS that are known to be safe to use with update_dps (0x12) command
|
||||
UPDATE_DPS_WHITELIST = [18, 19, 20] # Socket (Wi-Fi)
|
||||
|
||||
# This is intended to match requests.json payload at
|
||||
# https://github.com/codetheweb/tuyapi :
|
||||
# type_0a devices require the 0a command as the status request
|
||||
@@ -90,11 +95,13 @@ PAYLOAD_DICT = {
|
||||
STATUS: {"hexByte": 0x0A, "command": {"gwId": "", "devId": ""}},
|
||||
SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}},
|
||||
HEARTBEAT: {"hexByte": 0x09, "command": {}},
|
||||
UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}},
|
||||
},
|
||||
"type_0d": {
|
||||
STATUS: {"hexByte": 0x0D, "command": {"devId": "", "uid": "", "t": ""}},
|
||||
SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}},
|
||||
HEARTBEAT: {"hexByte": 0x09, "command": {}},
|
||||
UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -292,6 +299,8 @@ class MessageDispatcher(ContextualLogger):
|
||||
sem = self.listeners[self.HEARTBEAT_SEQNO]
|
||||
self.listeners[self.HEARTBEAT_SEQNO] = msg
|
||||
sem.release()
|
||||
elif msg.cmd == 0x12:
|
||||
self.debug("Got normal updatedps response")
|
||||
elif msg.cmd == 0x08:
|
||||
self.debug("Got status update")
|
||||
self.listener(msg)
|
||||
@@ -478,6 +487,26 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
||||
"""Send a heartbeat message."""
|
||||
return await self.exchange(HEARTBEAT)
|
||||
|
||||
async def update_dps(self, dps=None):
|
||||
"""
|
||||
Request device to update index.
|
||||
|
||||
Args:
|
||||
dps([int]): list of dps to update, default=detected&whitelisted
|
||||
"""
|
||||
if self.version == 3.3:
|
||||
if dps is None:
|
||||
if not self.dps_cache:
|
||||
await self.detect_available_dps()
|
||||
if self.dps_cache:
|
||||
dps = [int(dp) for dp in self.dps_cache]
|
||||
# filter non whitelisted dps
|
||||
dps = list(set(dps).intersection(set(UPDATE_DPS_WHITELIST)))
|
||||
self.debug("updatedps() entry (dps %s, dps_cache %s)", dps, self.dps_cache)
|
||||
payload = self._generate_payload(UPDATEDPS, dps)
|
||||
self.transport.write(payload)
|
||||
return True
|
||||
|
||||
async def set_dp(self, value, dp_index):
|
||||
"""
|
||||
Set value (may be any type: bool, int or string) of any dps index.
|
||||
@@ -582,6 +611,9 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
||||
json_data["t"] = str(int(time.time()))
|
||||
|
||||
if data is not None:
|
||||
if "dpId" in json_data:
|
||||
json_data["dpId"] = data
|
||||
else:
|
||||
json_data["dps"] = data
|
||||
elif command_hb == 0x0D:
|
||||
json_data["dps"] = self.dps_to_request
|
||||
@@ -591,7 +623,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
||||
|
||||
if self.version == 3.3:
|
||||
payload = self.cipher.encrypt(payload, False)
|
||||
if command_hb != 0x0A:
|
||||
if command_hb not in [0x0A, 0x12]:
|
||||
# add the 3.3 header
|
||||
payload = PROTOCOL_33_HEADER + payload
|
||||
elif command == SET:
|
||||
|
@@ -20,6 +20,7 @@
|
||||
"device_id": "Device ID",
|
||||
"local_key": "Local key",
|
||||
"protocol_version": "Protocol Version",
|
||||
"scan_interval": "Scan interval (seconds, only when not updating automatically)",
|
||||
"device_type": "Device type"
|
||||
}
|
||||
},
|
||||
|
@@ -29,7 +29,8 @@
|
||||
"host": "Host",
|
||||
"device_id": "Device ID",
|
||||
"local_key": "Local key",
|
||||
"protocol_version": "Protocol Version"
|
||||
"protocol_version": "Protocol Version",
|
||||
"scan_interval": "Scan interval (seconds, only when not updating automatically)"
|
||||
}
|
||||
},
|
||||
"pick_entity_type": {
|
||||
@@ -94,6 +95,7 @@
|
||||
"host": "Host",
|
||||
"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)"
|
||||
}
|
||||
},
|
||||
|
Reference in New Issue
Block a user