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."""
|
"""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]}"
|
||||||
|
|
||||||
|
@@ -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),
|
||||||
|
@@ -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:
|
||||||
|
@@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -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)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user