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

Add IR Button Support #1597

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions custom_components/localtuya/button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""Platform to present any Tuya DP as an enumeration."""
import logging
from functools import partial
import base64
import json
import struct
import voluptuous as vol
from homeassistant.components.button import DOMAIN, ButtonEntity
from homeassistant.const import (
CONF_DEVICE_CLASS
)

from .common import LocalTuyaEntity, async_setup_entry

from .const import (CONF_IR_BUTTON_B64, CONF_IR_BUTTON_PRONTO, CONF_IR_BUTTON_FRIENDLY, CONF_IR_DP_ID)


def flow_schema(dps):
"""Return schema used in config flow."""
return {
vol.Optional(CONF_IR_BUTTON_B64): str,
vol.Optional(CONF_IR_BUTTON_PRONTO): str,
vol.Required(CONF_IR_BUTTON_FRIENDLY): str,
vol.Required(CONF_IR_DP_ID): str,
}


_LOGGER = logging.getLogger(__name__)
NSDP_CONTROL = "control" # The control commands
NSDP_TYPE = "type" # The identifier of an IR library
NSDP_HEAD = "head" # Actually used but not documented
NSDP_KEY1 = "key1" # Actually used but not documented

class LocaltuyaIRButton(LocalTuyaEntity, ButtonEntity):
"""Representation of a Tuya Enumeration."""

def __init__(
self,
device,
config_entry,
sensorid,
**kwargs,
):
"""Initialize the Tuya sensor."""
dp_list = device.dps_to_request
generic_list = {}
for dp in list(dp_list):
generic_list[str(dp)] = "generic"

self._status = generic_list
self._default_status = generic_list

device._bypass_status = True
device._default_status = generic_list

super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs)

self._state = None
self._button_b64 = None
if CONF_IR_BUTTON_FRIENDLY in self._config:
self._button_b64 = self._config.get(CONF_IR_BUTTON_B64)
self._button_pronto = None
if CONF_IR_BUTTON_PRONTO in self._config:
self._button_pronto = self._config.get(CONF_IR_BUTTON_PRONTO)
self._default_dp = self._config.get(CONF_IR_DP_ID)

# Set Display options
self._display_options = []
display_options_str = ""
if CONF_IR_BUTTON_FRIENDLY in self._config:
display_options_str = self._config.get(CONF_IR_BUTTON_FRIENDLY).strip()

_LOGGER.debug("Button Configured: %s", display_options_str)

self._display_options.append(display_options_str)
_LOGGER.debug(
"Button Pronto Code: %s - B64: %s - Button Friendly: %s",
str(self._button_pronto),
str(self._button_b64),
str(self._display_options),
)

@property
def device_class(self):
"""Return the class of this device."""
return self._config.get(CONF_DEVICE_CLASS)

async def async_press(self) -> None:
"""Update the current value."""
base64_code = self._button_b64
if base64_code is None:
option_value = self._button_pronto
_LOGGER.debug("Sending Option: -> " + option_value)

pulses = self.pronto_to_pulses(option_value)
base64_code = '1' + self.pulses_to_base64(pulses)

await self.send_signal(base64_code)

def status_updated(self):
"""Device status was updated."""
super().status_updated()
self._status = self._default_status
self._state_friendly = "Generic Working"

# Default value is the first option
def entity_default_value(self):
"""Return the first option as the default value for this entity type."""
return self._button_pronto

'''
* Here Starts the journy of converting from pronto to a true IR Signal
'''

async def send_signal(self, base64_code):
command = {
NSDP_CONTROL: "send_ir",
NSDP_TYPE: 0,
}
command[NSDP_HEAD] = ''
command[NSDP_KEY1] = base64_code

await self._device.set_dp(json.dumps(command), self._default_dp)

def pronto_to_pulses(self, pronto):
ret = [ ]
pronto = [int(x, 16) for x in pronto.split(' ')]
ptype = pronto[0]
timebase = pronto[1]
pair1_len = pronto[2]
pair2_len = pronto[3]
if ptype != 0:
# only raw (learned) codes are handled
return ret
if timebase < 90 or timebase > 139:
# only 38 kHz is supported?
return ret
pronto = pronto[4:]
timebase *= 0.241246
for i in range(0, pair1_len*2, 2):
ret += [round(pronto[i] * timebase), round(pronto[i+1] * timebase)]
pronto = pronto[pair1_len*2:]
for i in range(0, pair2_len*2, 2):
ret += [round(pronto[i] * timebase), round(pronto[i+1] * timebase)]
return ret

def pulses_to_base64(self, pulses):
fmt = '<' + str(len(pulses)) + 'H'
return base64.b64encode( struct.pack(fmt, *pulses) ).decode("ascii")


async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaIRButton, flow_schema)
31 changes: 20 additions & 11 deletions custom_components/localtuya/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,12 @@ async def _make_connection(self):
try:
try:
self.debug("Retrieving initial state")
status = await self._interface.status()
if status is None:
raise Exception("Failed to retrieve status")
if hasattr(self, "_bypass_status") and self._bypass_status:
status = self._default_status
else:
status = await self._interface.status()
if status is None:
raise Exception("Failed to retrieve status")

self._interface.start_heartbeat()
self.status_updated(status)
Expand Down Expand Up @@ -403,15 +406,21 @@ async def async_added_to_hass(self):

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

# Update HA
if hasattr(self, "_bypass_status") and self._bypass_status:
status = self._default_status

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

# Update HA
self.schedule_update_ha_state()

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

Expand Down
7 changes: 7 additions & 0 deletions custom_components/localtuya/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# Platforms in this list must support config flows
PLATFORMS = [
"binary_sensor",
"button",
"climate",
"cover",
"fan",
Expand Down Expand Up @@ -134,3 +135,9 @@
# States
ATTR_STATE = "raw_state"
CONF_RESTORE_ON_RECONNECT = "restore_on_reconnect"

# IR Button
CONF_IR_BUTTON_B64 = "ir_b64_code"
CONF_IR_BUTTON_PRONTO = "ir_pronto_code"
CONF_IR_BUTTON_FRIENDLY = "ir_button_name"
CONF_IR_DP_ID = "ir_dp_id"
14 changes: 9 additions & 5 deletions custom_components/localtuya/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
},
"power_outlet": {
"title": "Add subswitch",
"description": "You are about to add subswitch number `{number}`. If you want to add another, tick `Add another switch` before continuing.",
"description": "You are about to add subswitch number `{number}`. If you want to add another, tick `Add another switch` before continuing.",
"data": {
"id": "ID",
"name": "Name",
Expand Down Expand Up @@ -101,10 +101,10 @@
"fan_speed_min": "minimum fan speed integer",
"fan_speed_max": "maximum fan speed integer",
"fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)",
"fan_direction": "fan direction dps",
"fan_direction":"fan direction dps",
"fan_direction_forward": "forward dps string",
"fan_direction_reverse": "reverse dps string",
"fan_dps_type": "DP value type",
"fan_dps_type": "DP value type",
"current_temperature_dp": "Current Temperature",
"target_temperature_dp": "Target Temperature",
"temperature_step": "Temperature Step (optional)",
Expand All @@ -126,7 +126,11 @@
"restore_on_reconnect": "Restore the last set value in HomeAssistant after a lost connection",
"min_value": "Minimum Value",
"max_value": "Maximum Value",
"step_size": "Minimum increment between numbers"
"step_size": "Minimum increment between numbers",
"ir_b64_code": "Button Code in Base 64 format",
"ir_pronto_code": "Button Code in Pronto format",
"ir_button_name": "User Friendly button name",
"ir_dp_id": "DP Id used to fire IR Signal"
}
},
"yaml_import": {
Expand All @@ -136,4 +140,4 @@
}
},
"title": "LocalTuya"
}
}
6 changes: 5 additions & 1 deletion custom_components/localtuya/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,11 @@
"min_value": "Minimum Value",
"max_value": "Maximum Value",
"step_size": "Minimum increment between numbers",
"is_passive_entity": "Passive entity - requires integration to send initialisation value"
"is_passive_entity": "Passive entity - requires integration to send initialisation value",
"ir_b64_code": "Button Code in Base 64 format",
"ir_pronto_code": "Button Code in Pronto format",
"ir_button_name": "User Friendly button name",
"ir_dp_id": "DP Id used to fire IR Signal"
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions custom_components/localtuya/translations/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,11 @@
"preset_set": "Set di preset (opzionale)",
"eco_dp": "DP per Eco (opzionale)",
"eco_value": "Valore Eco (opzionale)",
"heuristic_action": "Abilita azione euristica (opzionale)"
"heuristic_action": "Abilita azione euristica (opzionale)",
"ir_b64_code": "Codice pulsante in formato Base 64",
"ir_pronto_code": "Codice pulsante in formato Pronto",
"ir_button_name": "Nome pulsante intuitivo",
"ir_dp_id": "ID DP utilizzato per attivare il segnale IR"
}
}
}
Expand Down Expand Up @@ -213,4 +217,4 @@
}
},
"title": "LocalTuya"
}
}
8 changes: 6 additions & 2 deletions custom_components/localtuya/translations/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,11 @@
"preset_set": "Conjunto de predefinições (opcional)",
"eco_dp": "Eco DP (opcional)",
"eco_value": "Valor eco (opcional)",
"heuristic_action": "Ativar ação heurística (opcional)"
"heuristic_action": "Ativar ação heurística (opcional)",
"ir_b64_code": "Código Base 64 do botão",
"ir_pronto_code": "Código Pronto do botão",
"ir_button_name": "Nome do botão",
"ir_dp_id": "DP Utilizado para disparar o sinal"
}
}
}
Expand Down Expand Up @@ -213,4 +217,4 @@
}
},
"title": "LocalTuya"
}
}