From 4f979f0560e8c7c784e773c521d39e14d2b0990f Mon Sep 17 00:00:00 2001 From: root Date: Tue, 26 Dec 2023 18:02:50 +0100 Subject: [PATCH 1/2] Add IR Button Support --- custom_components/localtuya/button.py | 152 ++++++++++++++++++ custom_components/localtuya/common.py | 31 ++-- custom_components/localtuya/const.py | 7 + custom_components/localtuya/strings.json | 14 +- .../localtuya/translations/en.json | 6 +- .../localtuya/translations/it.json | 8 +- .../localtuya/translations/pt-BR.json | 8 +- 7 files changed, 205 insertions(+), 21 deletions(-) create mode 100644 custom_components/localtuya/button.py diff --git a/custom_components/localtuya/button.py b/custom_components/localtuya/button.py new file mode 100644 index 000000000..db7a9579e --- /dev/null +++ b/custom_components/localtuya/button.py @@ -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 = 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] = '1' + 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) \ No newline at end of file diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index cd503c2af..da3337f07 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -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) @@ -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]}" diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 630d630a3..819a48776 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -8,6 +8,7 @@ # Platforms in this list must support config flows PLATFORMS = [ "binary_sensor", + "button", "climate", "cover", "fan", @@ -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" \ No newline at end of file diff --git a/custom_components/localtuya/strings.json b/custom_components/localtuya/strings.json index 32f604002..549c1affe 100644 --- a/custom_components/localtuya/strings.json +++ b/custom_components/localtuya/strings.json @@ -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", @@ -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)", @@ -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": { @@ -136,4 +140,4 @@ } }, "title": "LocalTuya" -} \ No newline at end of file +} diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index cc289eea2..7510c3442 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -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" } } } diff --git a/custom_components/localtuya/translations/it.json b/custom_components/localtuya/translations/it.json index 264bb97fe..5e042232c 100644 --- a/custom_components/localtuya/translations/it.json +++ b/custom_components/localtuya/translations/it.json @@ -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": "Button Code in Base 64 format", + "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" } } } @@ -213,4 +217,4 @@ } }, "title": "LocalTuya" -} +} \ No newline at end of file diff --git a/custom_components/localtuya/translations/pt-BR.json b/custom_components/localtuya/translations/pt-BR.json index 74884ee6f..7adbce7b2 100644 --- a/custom_components/localtuya/translations/pt-BR.json +++ b/custom_components/localtuya/translations/pt-BR.json @@ -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": "Button Code in Base 64 format", + "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" } } } @@ -213,4 +217,4 @@ } }, "title": "LocalTuya" -} +} \ No newline at end of file From c9d5dc1421f50fdce22d925ffeda166e70c41ad7 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 26 Dec 2023 18:06:29 +0100 Subject: [PATCH 2/2] Better UX --- custom_components/localtuya/button.py | 4 ++-- custom_components/localtuya/translations/it.json | 2 +- custom_components/localtuya/translations/pt-BR.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/localtuya/button.py b/custom_components/localtuya/button.py index db7a9579e..50326a2e5 100644 --- a/custom_components/localtuya/button.py +++ b/custom_components/localtuya/button.py @@ -93,7 +93,7 @@ async def async_press(self) -> None: _LOGGER.debug("Sending Option: -> " + option_value) pulses = self.pronto_to_pulses(option_value) - base64_code = self.pulses_to_base64(pulses) + base64_code = '1' + self.pulses_to_base64(pulses) await self.send_signal(base64_code) @@ -118,7 +118,7 @@ async def send_signal(self, base64_code): NSDP_TYPE: 0, } command[NSDP_HEAD] = '' - command[NSDP_KEY1] = '1' + base64_code + command[NSDP_KEY1] = base64_code await self._device.set_dp(json.dumps(command), self._default_dp) diff --git a/custom_components/localtuya/translations/it.json b/custom_components/localtuya/translations/it.json index 5e042232c..48e37e17f 100644 --- a/custom_components/localtuya/translations/it.json +++ b/custom_components/localtuya/translations/it.json @@ -184,7 +184,7 @@ "eco_dp": "DP per Eco (opzionale)", "eco_value": "Valore Eco (opzionale)", "heuristic_action": "Abilita azione euristica (opzionale)", - "ir_b64_code": "Button Code in Base 64 format", + "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" diff --git a/custom_components/localtuya/translations/pt-BR.json b/custom_components/localtuya/translations/pt-BR.json index 7adbce7b2..6f7a1546b 100644 --- a/custom_components/localtuya/translations/pt-BR.json +++ b/custom_components/localtuya/translations/pt-BR.json @@ -184,7 +184,7 @@ "eco_dp": "Eco DP (opcional)", "eco_value": "Valor eco (opcional)", "heuristic_action": "Ativar ação heurística (opcional)", - "ir_b64_code": "Button Code in Base 64 format", + "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"