Merge pull request #549 from vmartinv/update_interval

Periodically send update dps command
This commit is contained in:
rospogrigio
2022-01-02 13:13:41 +01:00
committed by GitHub
5 changed files with 71 additions and 12 deletions

View File

@@ -1,6 +1,7 @@
"""Code shared between all platforms.""" """Code shared between all platforms."""
import asyncio import asyncio
import logging import logging
from datetime import timedelta
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_ID, CONF_DEVICE_ID,
@@ -9,8 +10,10 @@ from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_ID, CONF_ID,
CONF_PLATFORM, CONF_PLATFORM,
CONF_SCAN_INTERVAL,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_connect,
async_dispatcher_send, async_dispatcher_send,
@@ -117,6 +120,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
self._is_closing = False self._is_closing = False
self._connect_task = None self._connect_task = None
self._disconnect_task = None self._disconnect_task = None
self._unsub_interval = None
self.set_logger(_LOGGER, config_entry[CONF_DEVICE_ID]) self.set_logger(_LOGGER, config_entry[CONF_DEVICE_ID])
# This has to be done in case the device type is type_0d # 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._disconnect_task = async_dispatcher_connect(
self._hass, signal, _new_entity_handler 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 except Exception: # pylint: disable=broad-except
self.exception(f"Connect to {self._config_entry[CONF_HOST]} failed") self.exception(f"Connect to {self._config_entry[CONF_HOST]} failed")
if self._interface is not None: if self._interface is not None:
@@ -173,6 +187,10 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
self._interface = None self._interface = None
self._connect_task = 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): async def close(self):
"""Close connection and stop re-connect loop.""" """Close connection and stop re-connect loop."""
self._is_closing = True self._is_closing = True
@@ -223,7 +241,9 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
"""Device disconnected.""" """Device disconnected."""
signal = f"localtuya_{self._config_entry[CONF_DEVICE_ID]}" signal = f"localtuya_{self._config_entry[CONF_DEVICE_ID]}"
async_dispatcher_send(self._hass, signal, None) 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._interface = None
self.debug("Disconnected - waiting for discovery broadcast") self.debug("Disconnected - waiting for discovery broadcast")
@@ -253,13 +273,13 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
def _update_handler(status): def _update_handler(status):
"""Update entity state when status was updated.""" """Update entity state when status was updated."""
if status is not None: if status is None:
self._status = status status = {}
self.status_updated() if self._status != status:
else: self._status = status.copy()
self._status = {} if status:
self.status_updated()
self.schedule_update_ha_state() self.schedule_update_ha_state()
signal = f"localtuya_{self._config_entry.data[CONF_DEVICE_ID]}" signal = f"localtuya_{self._config_entry.data[CONF_DEVICE_ID]}"

View File

@@ -14,6 +14,7 @@ from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_ID, CONF_ID,
CONF_PLATFORM, CONF_PLATFORM,
CONF_SCAN_INTERVAL,
) )
from homeassistant.core import callback from homeassistant.core import callback
@@ -44,6 +45,7 @@ BASIC_INFO_SCHEMA = vol.Schema(
vol.Required(CONF_HOST): str, vol.Required(CONF_HOST): str,
vol.Required(CONF_DEVICE_ID): str, vol.Required(CONF_DEVICE_ID): str,
vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]), 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_LOCAL_KEY): cv.string,
vol.Required(CONF_FRIENDLY_NAME): cv.string, vol.Required(CONF_FRIENDLY_NAME): cv.string,
vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]), 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_HOST): str,
vol.Required(CONF_LOCAL_KEY): str, vol.Required(CONF_LOCAL_KEY): str,
vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]), vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]),
vol.Optional(CONF_SCAN_INTERVAL): int,
vol.Required( vol.Required(
CONF_ENTITIES, description={"suggested_value": entity_names} CONF_ENTITIES, description={"suggested_value": entity_names}
): cv.multi_select(entity_names), ): cv.multi_select(entity_names),

View File

@@ -21,6 +21,7 @@ Functions
json = status() # returns json payload json = status() # returns json payload
set_version(version) # 3.1 [default] or 3.3 set_version(version) # 3.1 [default] or 3.3
detect_available_dps() # returns a list of available dps provided by the device 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 add_dps_to_request(dp_index) # adds dp_index to the list of dps used by the
# device (to be queried in the payload) # device (to be queried in the payload)
set_dp(on, dp_index) # Set value of any dps index. 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" SET = "set"
STATUS = "status" STATUS = "status"
HEARTBEAT = "heartbeat" HEARTBEAT = "heartbeat"
UPDATEDPS = "updatedps" # Request refresh of DPS
PROTOCOL_VERSION_BYTES_31 = b"3.1" PROTOCOL_VERSION_BYTES_31 = b"3.1"
PROTOCOL_VERSION_BYTES_33 = b"3.3" PROTOCOL_VERSION_BYTES_33 = b"3.3"
@@ -76,6 +78,9 @@ SUFFIX_VALUE = 0x0000AA55
HEARTBEAT_INTERVAL = 10 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 # This is intended to match requests.json payload at
# https://github.com/codetheweb/tuyapi : # https://github.com/codetheweb/tuyapi :
# type_0a devices require the 0a command as the status request # type_0a devices require the 0a command as the status request
@@ -90,11 +95,13 @@ PAYLOAD_DICT = {
STATUS: {"hexByte": 0x0A, "command": {"gwId": "", "devId": ""}}, STATUS: {"hexByte": 0x0A, "command": {"gwId": "", "devId": ""}},
SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}}, SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}},
HEARTBEAT: {"hexByte": 0x09, "command": {}}, HEARTBEAT: {"hexByte": 0x09, "command": {}},
UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}},
}, },
"type_0d": { "type_0d": {
STATUS: {"hexByte": 0x0D, "command": {"devId": "", "uid": "", "t": ""}}, STATUS: {"hexByte": 0x0D, "command": {"devId": "", "uid": "", "t": ""}},
SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}}, SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}},
HEARTBEAT: {"hexByte": 0x09, "command": {}}, 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] sem = self.listeners[self.HEARTBEAT_SEQNO]
self.listeners[self.HEARTBEAT_SEQNO] = msg self.listeners[self.HEARTBEAT_SEQNO] = msg
sem.release() sem.release()
elif msg.cmd == 0x12:
self.debug("Got normal updatedps response")
elif msg.cmd == 0x08: elif msg.cmd == 0x08:
self.debug("Got status update") self.debug("Got status update")
self.listener(msg) self.listener(msg)
@@ -478,6 +487,26 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
"""Send a heartbeat message.""" """Send a heartbeat message."""
return await self.exchange(HEARTBEAT) 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): async def set_dp(self, value, dp_index):
""" """
Set value (may be any type: bool, int or string) of any dps index. Set value (may be any type: bool, int or string) of any dps index.
@@ -582,7 +611,10 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
json_data["t"] = str(int(time.time())) json_data["t"] = str(int(time.time()))
if data is not None: if data is not None:
json_data["dps"] = data if "dpId" in json_data:
json_data["dpId"] = data
else:
json_data["dps"] = data
elif command_hb == 0x0D: elif command_hb == 0x0D:
json_data["dps"] = self.dps_to_request json_data["dps"] = self.dps_to_request
@@ -591,7 +623,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
if self.version == 3.3: if self.version == 3.3:
payload = self.cipher.encrypt(payload, False) payload = self.cipher.encrypt(payload, False)
if command_hb != 0x0A: if command_hb not in [0x0A, 0x12]:
# add the 3.3 header # add the 3.3 header
payload = PROTOCOL_33_HEADER + payload payload = PROTOCOL_33_HEADER + payload
elif command == SET: elif command == SET:

View File

@@ -20,6 +20,7 @@
"device_id": "Device ID", "device_id": "Device ID",
"local_key": "Local key", "local_key": "Local key",
"protocol_version": "Protocol Version", "protocol_version": "Protocol Version",
"scan_interval": "Scan interval (seconds, only when not updating automatically)",
"device_type": "Device type" "device_type": "Device type"
} }
}, },
@@ -39,4 +40,4 @@
} }
}, },
"title": "LocalTuya" "title": "LocalTuya"
} }

View File

@@ -29,7 +29,8 @@
"host": "Host", "host": "Host",
"device_id": "Device ID", "device_id": "Device ID",
"local_key": "Local key", "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": { "pick_entity_type": {
@@ -94,6 +95,7 @@
"host": "Host", "host": "Host",
"local_key": "Local key", "local_key": "Local key",
"protocol_version": "Protocol Version", "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)"
} }
}, },