Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Periodically send update dps command #549

Merged
merged 14 commits into from
Jan 2, 2022
36 changes: 28 additions & 8 deletions custom_components/localtuya/common.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Code shared between all platforms."""
import asyncio
import logging
from datetime import timedelta

from homeassistant.const import (
CONF_DEVICE_ID,
Expand All @@ -9,8 +10,10 @@
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,
Expand Down Expand Up @@ -117,6 +120,7 @@ def __init__(self, hass, config_entry):
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
Expand Down Expand Up @@ -166,13 +170,27 @@ def _new_entity_handler(entity_id):
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:
await self._interface.close()
self._interface = None
self._connect_task = None

async def _async_refresh(self, _now):
if self._interface is not None:
await self._interface.update_dps()

Copy link

@jeremysherriff jeremysherriff Nov 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vmartinv can you put this in for others to test with?
Edit: Updated, inelegant, but seems functional

            # await self._interface.update_dps()
            dps_blacklist = [1,9,21] #Socket (Wi-Fi)
            if isinstance(self.dps_to_request, int):
                dps_to_update = [ self.dps_to_request ]
            else:
                dps_to_update = []
                for index in self.dps_to_request:
                    if not index in dps_blacklist:
                        dps_to_update.append(index)
            await self._interface.update_dps( dps_to_update )

async def close(self):
"""Close connection and stop re-connect loop."""
self._is_closing = True
Expand Down Expand Up @@ -223,7 +241,9 @@ def disconnected(self):
"""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")

Expand Down Expand Up @@ -253,13 +273,13 @@ async def async_added_to_hass(self):

def _update_handler(status):
"""Update entity state when status was updated."""
if status is not None:
self._status = status
self.status_updated()
else:
self._status = {}

self.schedule_update_ha_state()
if status is None:
status = {}
if self._status != status:
self._status = status.copy()
if status:
self.status_updated()
self.schedule_update_ha_state()

signal = f"localtuya_{self._config_entry.data[CONF_DEVICE_ID]}"

Expand Down
4 changes: 4 additions & 0 deletions custom_components/localtuya/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
CONF_HOST,
CONF_ID,
CONF_PLATFORM,
CONF_SCAN_INTERVAL,
)
from homeassistant.core import callback

Expand Down Expand Up @@ -44,6 +45,7 @@
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,
}
)

Expand All @@ -55,6 +57,7 @@
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,
}
)

Expand Down Expand Up @@ -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),
Expand Down
36 changes: 34 additions & 2 deletions custom_components/localtuya/pytuya/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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.
Expand Down Expand Up @@ -61,6 +62,7 @@
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"
Expand All @@ -76,6 +78,9 @@

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
Expand All @@ -90,11 +95,13 @@
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]}},
},
}

Expand Down Expand Up @@ -292,6 +299,8 @@ def _dispatch(self, msg):
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)
Expand Down Expand Up @@ -478,6 +487,26 @@ async def heartbeat(self):
"""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.
Expand Down Expand Up @@ -582,7 +611,10 @@ def _generate_payload(self, command, data=None):
json_data["t"] = str(int(time.time()))

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:
json_data["dps"] = self.dps_to_request

Expand All @@ -591,7 +623,7 @@ def _generate_payload(self, command, data=None):

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:
Expand Down
3 changes: 2 additions & 1 deletion custom_components/localtuya/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
Expand All @@ -39,4 +40,4 @@
}
},
"title": "LocalTuya"
}
}
4 changes: 3 additions & 1 deletion custom_components/localtuya/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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)"
}
},
Expand Down