Skip to content

Commit

Permalink
feat: add ble device
Browse files Browse the repository at this point in the history
This PR modifies the changes made in the original PR by @Lurker00

Co-authored-by: Lurker00 <[email protected]>
  • Loading branch information
xZetsubou and Lurker00 authored Jan 8, 2025
1 parent c2159d7 commit 4ecf43d
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 114 deletions.
2 changes: 1 addition & 1 deletion custom_components/localtuya/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -1101,7 +1101,7 @@ def dps_string_list(dps_data: dict[str, dict], cloud_dp_codes: dict[str, dict])
for dp, func in cloud_dp_codes.items():
# Default Manual dp value is -1, we will replace it if it in cloud.
add_dp = dp not in dps_data or dps_data.get(dp) == -1
if add_dp and ((value := func.get("value")) or value is not None):
if (value := func.get("value", "")) or add_dp:
dps_data[dp] = f"{value}, cloud pull"

for dp, value in dps_data.items():
Expand Down
12 changes: 12 additions & 0 deletions custom_components/localtuya/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ def is_sleep(self):

return device_sleep > 0 and is_sleep

@property
def is_write_only(self):
"""Return if this sub-device is BLE. We uses 0 in manual dps as mark for BLE devices.
NOTE: this may not be the best way to detect if this device is BLE
"""
return self.is_subdevice and "0" in self._device_config.manual_dps.split(",")

def add_entities(self, entities):
"""Set the entities associated with this device."""
self._entities.extend(entities)
Expand Down Expand Up @@ -414,6 +422,10 @@ async def set_status(self):
payload, self._pending_status = self._pending_status.copy(), {}
try:
await self._interface.set_dps(payload, cid=self._node_id)
# bluetooth devices usually does not send updated status payload.
# NOTE: This will override the status if the BLE device fails to receive the signal.
if self.is_write_only:
self.status_updated(payload)
except Exception as ex: # pylint: disable=broad-except
self.debug(f"Failed to set values {payload} --> {ex}", force=True)
elif not self.connected:
Expand Down
2 changes: 2 additions & 0 deletions custom_components/localtuya/core/ha_entities/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ class DPCode(StrEnum):
COLOUR_DATA = "colour_data" # Colored light mode
COLOUR_DATA_HSV = "colour_data_hsv" # Colored light mode
COLOUR_DATA_V2 = "colour_data_v2" # Colored light mode
COLOUR_DATA_RAW = "colour_data_raw" # Colored light mode for BLE
COMPRESSOR_COMMAND = "compressor_command"
CONCENTRATION_SET = "concentration_set" # Concentration setting
CONTROL = "control"
Expand Down Expand Up @@ -505,6 +506,7 @@ class DPCode(StrEnum):
SCENE_9 = "scene_9"
SCENE_DATA = "scene_data" # Colored light mode
SCENE_DATA_V2 = "scene_data_v2" # Colored light mode
SCENE_DATA_RAW = "scene_data_raw" # Colored light mode for BLE
SEEK = "seek"
SENS = "sens" # Ikuu SXSEN003PIR IP65 Motion Detector (Wi-Fi)
SENSITIVITY = "sensitivity" # Sensitivity
Expand Down
4 changes: 2 additions & 2 deletions custom_components/localtuya/core/ha_entities/lights.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ def localtuya_light(
color_mode=DPCode.WORK_MODE,
brightness=(DPCode.BRIGHT_VALUE_V2, DPCode.BRIGHT_VALUE),
color_temp=(DPCode.TEMP_VALUE_V2, DPCode.TEMP_VALUE),
color=(DPCode.COLOUR_DATA_V2, DPCode.COLOUR_DATA),
scene=(DPCode.SCENE_DATA_V2, DPCode.SCENE_DATA),
color=(DPCode.COLOUR_DATA_V2, DPCode.COLOUR_DATA, DPCode.COLOUR_DATA_RAW),
scene=(DPCode.SCENE_DATA_V2, DPCode.SCENE_DATA, DPCode.SCENE_DATA_RAW),
custom_configs=localtuya_light(29, 1000, 2700, 6500, False, True),
),
# Not documented
Expand Down
36 changes: 24 additions & 12 deletions custom_components/localtuya/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def __init__(
self._last_state = None
self._stored_states: State | None = None
self._hass = device._hass
self._loaded = False

# Default value is available to be provided by Platform entities if required
self._default_value = self._config.get(CONF_DEFAULT_VALUE)
Expand All @@ -160,23 +161,20 @@ async def async_added_to_hass(self):
self._stored_states = stored_data
self.status_restored(stored_data)

def _update_handler(new_status: dict | None):
def _update_handler(status: dict | None):
"""Update entity state when status was updated."""
status = self._status.clear() if new_status is None else new_status.copy()
last_status = self._status.copy()

if status == RESTORE_STATES and stored_data and not self._status:
if stored_data.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN):
self.debug(f"{self.name}: Restore state: {stored_data.state}")
status[self._dp_id] = stored_data.state
self._status = {} if status is None else {**self._status, **status}

if self._status != status:
if not self._loaded:
self._loaded = True
self.connection_made()

if status != last_status:
if status:
# Pop the special DPs
status.pop("0", None)
self._status.update(status)
self.status_updated()

# Update HA
self.schedule_update_ha_state()

signal = f"localtuya_{self._device_config.id}"
Expand Down Expand Up @@ -297,7 +295,7 @@ def status_updated(self) -> None:
if (state is not None) and (not self._device.is_connecting):
self._last_state = state

def status_restored(self, stored_state) -> None:
def status_restored(self, stored_state: State) -> None:
"""Device status was restored.
Override in subclasses and update entity specific state.
Expand All @@ -309,6 +307,20 @@ def status_restored(self, stored_state) -> None:
f"Restoring state for entity: {self.name} - state: {str(self._last_state)}"
)

def connection_made(self):
"""The connection has made with the device and status retrieved. configure entity based on it.
Override in subclasses and update entity initialization based on detected DPS.
"""
stored_data = self._stored_states
if self._status == RESTORE_STATES and stored_data:
self._status.pop("0", True)
if self._dp_id in self._status:
return
if stored_data.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN):
self.debug(f"{self.name}: Restore state: {stored_data.state}")
self._status[self._dp_id] = stored_data.state

def default_value(self):
"""Return default value of this entity.
Expand Down
Loading

0 comments on commit 4ecf43d

Please sign in to comment.