|
|
@@ -16,6 +16,7 @@ import voluptuous as vol
|
|
|
from homeassistant.components import persistent_notification
|
|
|
from homeassistant.components.remote import (
|
|
|
ATTR_ALTERNATIVE,
|
|
|
+ ATTR_COMMAND_TYPE,
|
|
|
ATTR_DELAY_SECS,
|
|
|
ATTR_DEVICE,
|
|
|
ATTR_NUM_REPEATS,
|
|
|
@@ -56,9 +57,12 @@ LEARNING_TIMEOUT = timedelta(seconds=30)
|
|
|
|
|
|
# These commands seem to be standard for all devices
|
|
|
CMD_SEND = "send_ir"
|
|
|
+CMD_SEND_RF = "rfstudy_send"
|
|
|
CMD_LEARN = "study"
|
|
|
CMD_ENDLEARN = "study_exit"
|
|
|
CMD_STUDYKEY = "study_key"
|
|
|
+CMD_STUDYRF = "rf_study"
|
|
|
+CMD_ENDSTUDYRF = "rfstudy_exit"
|
|
|
|
|
|
COMMAND_SCHEMA = vol.Schema(
|
|
|
{
|
|
|
@@ -146,7 +150,9 @@ class TuyaLocalRemote(TuyaLocalEntity, RemoteEntity):
|
|
|
|
|
|
def _extract_codes(self, commands, subdevice=None):
|
|
|
"""Extract a list of remote codes.
|
|
|
- If the command starts with 'b64:', extract the code from it.
|
|
|
+ If the command starts with 'b64:', extract the IR code from it.
|
|
|
+ If the command starts with 'rf:', keep it as-is so that
|
|
|
+ _encode_send_code can apply the correct RF payload format.
|
|
|
Otherwise use the command and optionally subdevice as keys to extract the
|
|
|
actual command from storage.
|
|
|
|
|
|
@@ -156,6 +162,8 @@ class TuyaLocalRemote(TuyaLocalEntity, RemoteEntity):
|
|
|
for cmd in commands:
|
|
|
if cmd.startswith("b64:"):
|
|
|
codes = [cmd[4:]]
|
|
|
+ elif cmd.startswith("rf:"):
|
|
|
+ codes = [cmd]
|
|
|
else:
|
|
|
if subdevice is None:
|
|
|
raise ValueError("device must be specified")
|
|
|
@@ -179,23 +187,52 @@ class TuyaLocalRemote(TuyaLocalEntity, RemoteEntity):
|
|
|
code_list.append(codes)
|
|
|
return code_list
|
|
|
|
|
|
- def _encode_send_code(self, code, delay):
|
|
|
- """Encode a remote command into dps values to send."""
|
|
|
- # Based on https://github.com/jasonacox/tinytuya/issues/74 and
|
|
|
- # the docs it references, there are two kinds of IR devices.
|
|
|
- # 1. separate dps for control, code, study,...
|
|
|
- # 2. single dp (201) for send_ir, which takes JSON input,
|
|
|
- # including control, code, delay, etc, and another for
|
|
|
- # study_ir (202) that receives the codes in study mode.
|
|
|
+ def _encode_send_code(self, code, delay, is_rf=False):
|
|
|
+ """Encode a remote command into dps values to send.
|
|
|
+
|
|
|
+ Set is_rf=True to use the RF sub-GHz payload format.
|
|
|
+ The default (is_rf=False) uses the IR payload format.
|
|
|
+
|
|
|
+ Based on https://github.com/jasonacox/tinytuya/issues/74 and
|
|
|
+ the docs it references, there are two kinds of IR devices.
|
|
|
+ 1. separate dps for control, code, study,...
|
|
|
+ 2. single dp (201) for send_ir, which takes JSON input,
|
|
|
+ including control, code, delay, etc, and another for
|
|
|
+ study_ir (202) that receives the codes in study mode.
|
|
|
+ RF devices also use a single dp (201) but with a different
|
|
|
+ JSON payload using control 'rfstudy_send'.
|
|
|
+ """
|
|
|
dps = {}
|
|
|
if self._control_dp:
|
|
|
- # control and code are sent in seperate dps.
|
|
|
+ # control and code are sent in separate dps.
|
|
|
dps = dps | self._control_dp.get_values_to_set(self._device, CMD_SEND, dps)
|
|
|
dps = dps | self._send_dp.get_values_to_set(self._device, code, dps)
|
|
|
if self._delay_dp:
|
|
|
dps = dps | self._delay_dp.get_values_to_set(self._device, delay, dps)
|
|
|
if self._type_dp:
|
|
|
dps = dps | self._type_dp.get_values_to_set(self._device, 0, dps)
|
|
|
+ elif is_rf:
|
|
|
+ dps = dps | self._send_dp.get_values_to_set(
|
|
|
+ self._device,
|
|
|
+ json.dumps(
|
|
|
+ {
|
|
|
+ "control": CMD_SEND_RF,
|
|
|
+ "rf_type": "sub_2g",
|
|
|
+ "mode": 0,
|
|
|
+ "key1": {
|
|
|
+ "times": 6,
|
|
|
+ "intervals": 0,
|
|
|
+ "ver": "2",
|
|
|
+ "delay": 0,
|
|
|
+ "code": code,
|
|
|
+ },
|
|
|
+ "feq": 0,
|
|
|
+ "rate": 0,
|
|
|
+ "ver": "2",
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ dps,
|
|
|
+ )
|
|
|
else:
|
|
|
dps = dps | self._send_dp.get_values_to_set(
|
|
|
self._device,
|
|
|
@@ -203,7 +240,7 @@ class TuyaLocalRemote(TuyaLocalEntity, RemoteEntity):
|
|
|
{
|
|
|
"control": CMD_SEND,
|
|
|
"head": "",
|
|
|
- # leading zero means use head, any other leeading character is discarded.
|
|
|
+ # leading zero means use head, any other leading character is discarded.
|
|
|
"key1": "1" + code,
|
|
|
"type": 0,
|
|
|
"delay": int(delay),
|
|
|
@@ -241,7 +278,10 @@ class TuyaLocalRemote(TuyaLocalEntity, RemoteEntity):
|
|
|
else:
|
|
|
code = codes[0]
|
|
|
|
|
|
- dps_to_set = self._encode_send_code(code, delay)
|
|
|
+ if code.startswith("rf:"):
|
|
|
+ dps_to_set = self._encode_send_code(code[3:], delay, is_rf=True)
|
|
|
+ else:
|
|
|
+ dps_to_set = self._encode_send_code(code, delay)
|
|
|
await self._device.async_set_properties(dps_to_set)
|
|
|
|
|
|
if len(codes) > 1:
|
|
|
@@ -249,7 +289,7 @@ class TuyaLocalRemote(TuyaLocalEntity, RemoteEntity):
|
|
|
at_least_one_sent = True
|
|
|
|
|
|
if at_least_one_sent:
|
|
|
- self._flag_storage.async_delay_save(self._flags, FLAG_SAVE_DELAY)
|
|
|
+ self._flag_storage.async_delay_save(lambda: self._flags, FLAG_SAVE_DELAY)
|
|
|
|
|
|
async def async_learn_command(self, **kwargs: Any) -> None:
|
|
|
"""Learn a list of commands from a remote."""
|
|
|
@@ -257,6 +297,7 @@ class TuyaLocalRemote(TuyaLocalEntity, RemoteEntity):
|
|
|
commands = kwargs[ATTR_COMMAND]
|
|
|
subdevice = kwargs[ATTR_DEVICE]
|
|
|
toggle = kwargs[ATTR_ALTERNATIVE]
|
|
|
+ is_rf = kwargs.get(ATTR_COMMAND_TYPE) == "rf"
|
|
|
|
|
|
if not self._storage_loaded:
|
|
|
await self._async_load_storage()
|
|
|
@@ -265,24 +306,43 @@ class TuyaLocalRemote(TuyaLocalEntity, RemoteEntity):
|
|
|
should_store = False
|
|
|
|
|
|
for command in commands:
|
|
|
- code = await self._async_learn_command(command)
|
|
|
+ code = await self._async_learn_command(command, is_rf=is_rf)
|
|
|
_LOGGER.info("Learning %s for %s: %s", command, subdevice, code)
|
|
|
# pulses = base64_to_pulses(code)
|
|
|
# _LOGGER.debug("= pronto code: %s", pulses_to_pronto(pulses))
|
|
|
# _LOGGER.debug("= width encoded: %s", pulses_to_width_encoded(pulses))
|
|
|
if toggle:
|
|
|
- code = [code, await self._async_learn_command(command)]
|
|
|
+ code = [code, await self._async_learn_command(command, is_rf=is_rf)]
|
|
|
self._codes.setdefault(subdevice, {}).update({command: code})
|
|
|
should_store = True
|
|
|
|
|
|
if should_store:
|
|
|
await self._code_storage.async_save(self._codes)
|
|
|
|
|
|
- async def _async_learn_command(self, command):
|
|
|
+ async def _async_learn_command(self, command, is_rf=False):
|
|
|
"""Learn a single command"""
|
|
|
service = f"{RM_DOMAIN}.{SERVICE_LEARN_COMMAND}"
|
|
|
+ if is_rf:
|
|
|
+ cmd_start = json.dumps(
|
|
|
+ {
|
|
|
+ "control": CMD_STUDYRF,
|
|
|
+ "rf_type": "sub_2g",
|
|
|
+ "study_feq": "0",
|
|
|
+ "ver": "2",
|
|
|
+ }
|
|
|
+ )
|
|
|
+ cmd_end = json.dumps(
|
|
|
+ {
|
|
|
+ "control": CMD_ENDSTUDYRF,
|
|
|
+ "rf_type": "sub_2g",
|
|
|
+ "study_feq": "0",
|
|
|
+ "ver": "2",
|
|
|
+ }
|
|
|
+ )
|
|
|
if self._control_dp:
|
|
|
await self._control_dp.async_set_value(self._device, CMD_LEARN)
|
|
|
+ elif is_rf:
|
|
|
+ await self._send_dp.async_set_value(self._device, cmd_start)
|
|
|
else:
|
|
|
await self._send_dp.async_set_value(
|
|
|
self._device,
|
|
|
@@ -301,7 +361,8 @@ class TuyaLocalRemote(TuyaLocalEntity, RemoteEntity):
|
|
|
await asyncio.sleep(1)
|
|
|
code = self._receive_dp.get_value(self._device)
|
|
|
if code is not None:
|
|
|
- return code
|
|
|
+ self._device.anticipate_property_value(self._receive_dp.id, None)
|
|
|
+ return "rf:" + code if is_rf else code
|
|
|
_LOGGER.warning("Timed out without receiving code in %s", service)
|
|
|
raise TimeoutError(
|
|
|
f"No remote code received within {LEARNING_TIMEOUT.total_seconds()} seconds",
|
|
|
@@ -316,6 +377,8 @@ class TuyaLocalRemote(TuyaLocalEntity, RemoteEntity):
|
|
|
self._device,
|
|
|
CMD_ENDLEARN,
|
|
|
)
|
|
|
+ elif is_rf:
|
|
|
+ await self._send_dp.async_set_value(self._device, cmd_end)
|
|
|
else:
|
|
|
await self._send_dp.async_set_value(
|
|
|
self._device,
|
|
|
@@ -362,5 +425,7 @@ class TuyaLocalRemote(TuyaLocalEntity, RemoteEntity):
|
|
|
if not codes:
|
|
|
del self._codes[subdevice]
|
|
|
if self._flags.pop(subdevice, None) is not None:
|
|
|
- self._flag_storage.async_delay_save(self._flags, FLAG_SAVE_DELAY)
|
|
|
- self._code_storage.async_delay_save(self._codes, CODE_SAVE_DELAY)
|
|
|
+ self._flag_storage.async_delay_save(
|
|
|
+ lambda: self._flags, FLAG_SAVE_DELAY
|
|
|
+ )
|
|
|
+ self._code_storage.async_delay_save(lambda: self._codes, CODE_SAVE_DELAY)
|