Skip to content

Commit

Permalink
Update to version 0.1.7
Browse files Browse the repository at this point in the history
### Added

- Added new product_ids.
- Added full support of BLE TRV provided by @forabi
- Added support of programming mode for Fingerbot Plus, thanks @redphx for information.

### Changed

- Improved connection stability.
  • Loading branch information
PlusPlus-ua committed Jun 29, 2023
1 parent 2abd0b9 commit c11a1c5
Show file tree
Hide file tree
Showing 14 changed files with 562 additions and 142 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,15 @@ and this project adheres to [Semantic Versioning].

- Updated sources to conform Python 3.11

## [0.1.7] - 2023-06-01

### Added

- Added new product_ids.
- Added full support of BLE TRV provided by @forabi
- Added support of programming mode for Fingerbot Plus, thanks @redphx for information.

### Changed

- Improved connection stability.

7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ The integration works locally, but connection to Tuya BLE device requires device
* Fingerbots (category_id 'szjqr')
+ Fingerbot (product_ids 'ltak7e1p', 'y6kttvd6', 'yrnk7mnn', 'nvr2rocq', 'bnt7wajf', 'rvdceqjh', '5xhbk964'), original device, first in category, powered by CR2 battery.
+ Adaprox Fingerbot (product_id 'y6kttvd6'), built-in battery with USB type C charging.
+ Fingerbot Plus (product_ids 'blliqpsj', 'yiihr7zh', 'neq16kgd'), almost same as original, has sensor button for manual control.
+ Fingerbot Plus (product_ids 'blliqpsj', 'ndvkgsrm', 'yiihr7zh', 'neq16kgd'), almost same as original, has sensor button for manual control.
+ CubeTouch 1s (product_id '3yqdo5yt'), built-in battery with USB type C charging.
+ CubeTouch II (product_id 'xhf790if'), built-in battery with USB type C charging.

All features available in Home Assistant, except programming (series of actions) - it's not documented and looks useless because it could be implemented by Home Assistant scripts or automations.
All features available in Home Assistant, programming (series of actions) is implemented for Fingerbot Plus.
For programming exposed entities 'Program' (switch), 'Repeat forever', 'Repeats count', 'Idle position' and 'Program' (text). Format of program text is: 'position\[/time\];...' where position is in percents, optional time is in seconds (zero if missing).

* Temperature and humidity sensors (category_id 'wsdcg')
+ Soil moisture sensor (product_id 'ojzlzzsw').
Expand All @@ -39,7 +40,7 @@ The integration works locally, but connection to Tuya BLE device requires device
+ Smart Lock (product_id 'ludzroix'), first attempt to support for now.

* Climate (category_id 'wk')
+ Thermostatic Radiator Valve (product_ids 'drlajpqc', 'nhj2j7su'), first attempt to support for now.
+ Thermostatic Radiator Valve (product_ids 'drlajpqc', 'nhj2j7su').

* Smart water bottle (category_id 'znhsb')
+ Smart water bottle (product_id 'cdlandip')
Expand Down
1 change: 1 addition & 0 deletions custom_components/tuya_ble/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
Platform.BINARY_SENSOR,
Platform.SELECT,
Platform.SWITCH,
Platform.TEXT,
]

_LOGGER = logging.getLogger(__name__)
Expand Down
12 changes: 7 additions & 5 deletions custom_components/tuya_ble/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ class TuyaBLEBinarySensorMapping:
force_add: bool = True
dp_type: TuyaBLEDataPointType | None = None
getter: Callable[[TuyaBLEBinarySensor], None] | None = None
coefficient: float = 1.0
icons: list[str] | None = None
#coefficient: float = 1.0
#icons: list[str] | None = None
is_available: TuyaBLEBinarySensorIsAvailable = None


Expand All @@ -58,11 +58,10 @@ class TuyaBLECategoryBinarySensorMapping:
TuyaBLEBinarySensorMapping(
dp_id=105,
description=BinarySensorEntityDescription(
key="low_battery",
icon="mdi:battery-alert",
key="battery",
#icon="mdi:battery-alert",
device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=True,
),
),
],
Expand Down Expand Up @@ -107,6 +106,8 @@ def _handle_coordinator_update(self) -> None:
else:
datapoint = self._device.datapoints[self._mapping.dp_id]
if datapoint:
self._attr_is_on = bool(datapoint.value)
'''
if datapoint.type == TuyaBLEDataPointType.DT_ENUM:
if self.entity_description.options is not None:
if datapoint.value >= 0 and datapoint.value < len(
Expand All @@ -128,6 +129,7 @@ def _handle_coordinator_update(self) -> None:
)
else:
self._attr_native_value = datapoint.value
'''
self.async_write_ha_state()

@property
Expand Down
3 changes: 3 additions & 0 deletions custom_components/tuya_ble/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class TuyaBLEFingerbotInfo:
hold_time: int
reverse_positions: int
manual_control: int = 0
program: int = 0


@dataclass
Expand Down Expand Up @@ -228,6 +229,7 @@ class TuyaBLECategoryInfo:
hold_time=10,
reverse_positions=11,
manual_control=17,
program=121,
),
),
),
Expand All @@ -250,6 +252,7 @@ class TuyaBLECategoryInfo:
down_position=9,
hold_time=10,
reverse_positions=11,
program=121,
),
),
),
Expand Down
2 changes: 1 addition & 1 deletion custom_components/tuya_ble/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/tuya_ble",
"requirements": ["tuya-iot-py-sdk==0.6.6", "pycountry==22.3.5"],
"iot_class": "local_push",
"version": "0.1.6"
"version": "0.1.7"
}
152 changes: 123 additions & 29 deletions custom_components/tuya_ble/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@


TuyaBLENumberSetter = (
Callable[["TuyaBLENumber", TuyaBLEProductInfo, float], bool] | None
Callable[["TuyaBLENumber", TuyaBLEProductInfo, float], None] | None
)


Expand All @@ -60,25 +60,34 @@ class TuyaBLENumberMapping:
mode: NumberMode = NumberMode.BOX


def is_fingerbot_not_in_program_mode(self: TuyaBLENumber, product: TuyaBLEProductInfo) -> bool:
def is_fingerbot_in_program_mode(
self: TuyaBLENumber,
product: TuyaBLEProductInfo,
) -> bool:
result: bool = True
if product.fingerbot:
datapoint = self._device.datapoints[product.fingerbot.mode]
if datapoint:
result = datapoint.value != 2
result = datapoint.value == 2
return result


def is_fingerbot_in_program_mode(self: TuyaBLENumber, product: TuyaBLEProductInfo) -> bool:
def is_fingerbot_not_in_program_mode(
self: TuyaBLENumber,
product: TuyaBLEProductInfo,
) -> bool:
result: bool = True
if product.fingerbot:
datapoint = self._device.datapoints[product.fingerbot.mode]
if datapoint:
result = datapoint.value == 2
result = datapoint.value != 2
return result


def is_fingerbot_in_push_mode(self: TuyaBLENumber, product: TuyaBLEProductInfo) -> bool:
def is_fingerbot_in_push_mode(
self: TuyaBLENumber,
product: TuyaBLEProductInfo,
) -> bool:
result: bool = True
if product.fingerbot:
datapoint = self._device.datapoints[product.fingerbot.mode]
Expand All @@ -87,6 +96,79 @@ def is_fingerbot_in_push_mode(self: TuyaBLENumber, product: TuyaBLEProductInfo)
return result


def is_fingerbot_repeat_count_available(
self: TuyaBLENumber,
product: TuyaBLEProductInfo,
) -> bool:
result: bool = True
if product.fingerbot and product.fingerbot.program:
datapoint = self._device.datapoints[product.fingerbot.mode]
if datapoint:
result = datapoint.value == 2
if result:
datapoint = self._device.datapoints[product.fingerbot.program]
if datapoint and type(datapoint.value) is bytes:
repeat_count = int.from_bytes(datapoint.value[0:2], "big")
result = repeat_count != 0xFFFF

return result


def get_fingerbot_program_repeat_count(
self: TuyaBLENumber,
product: TuyaBLEProductInfo,
) -> float | None:
result: float | None = None
if product.fingerbot and product.fingerbot.program:
datapoint = self._device.datapoints[product.fingerbot.program]
if datapoint and type(datapoint.value) is bytes:
repeat_count = int.from_bytes(datapoint.value[0:2], "big")
result = repeat_count * 1.0

return result


def set_fingerbot_program_repeat_count(
self: TuyaBLENumber,
product: TuyaBLEProductInfo,
value: float,
) -> None:
if product.fingerbot and product.fingerbot.program:
datapoint = self._device.datapoints[product.fingerbot.program]
if datapoint and type(datapoint.value) is bytes:
new_value = (
int.to_bytes(int(value), 2, "big") +
datapoint.value[2:]
)
self._hass.create_task(datapoint.set_value(new_value))


def get_fingerbot_program_position(
self: TuyaBLENumber,
product: TuyaBLEProductInfo,
) -> float | None:
result: float | None = None
if product.fingerbot and product.fingerbot.program:
datapoint = self._device.datapoints[product.fingerbot.program]
if datapoint and type(datapoint.value) is bytes:
result = datapoint.value[2] * 1.0

return result


def set_fingerbot_program_position(
self: TuyaBLENumber,
product: TuyaBLEProductInfo,
value: float,
) -> None:
if product.fingerbot and product.fingerbot.program:
datapoint = self._device.datapoints[product.fingerbot.program]
if datapoint and type(datapoint.value) is bytes:
new_value = bytearray(datapoint.value)
new_value[2] = int(value)
self._hass.create_task(datapoint.set_value(new_value))


@dataclass
class TuyaBLEDownPositionDescription(NumberEntityDescription):
key: str = "down_position"
Expand Down Expand Up @@ -207,7 +289,7 @@ class TuyaBLECategoryNumberMapping:
[
"blliqpsj",
"ndvkgsrm",
"yiihr7zh",
"yiihr7zh",
"neq16kgd"
], # Fingerbot Plus
[
Expand All @@ -222,6 +304,35 @@ class TuyaBLECategoryNumberMapping:
description=TuyaBLEUpPositionDescription(),
is_available=is_fingerbot_not_in_program_mode,
),
TuyaBLENumberMapping(
dp_id=121,
description=NumberEntityDescription(
key="program_repeats_count",
icon="mdi:repeat",
native_max_value=0xFFFE,
native_min_value=1,
native_step=1,
entity_category=EntityCategory.CONFIG,
),
is_available=is_fingerbot_repeat_count_available,
getter=get_fingerbot_program_repeat_count,
setter=set_fingerbot_program_repeat_count,
),
TuyaBLENumberMapping(
dp_id=121,
description=NumberEntityDescription(
key="program_idle_position",
icon="mdi:repeat",
native_max_value=100,
native_min_value=0,
native_step=1,
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.CONFIG,
),
is_available=is_fingerbot_in_program_mode,
getter=get_fingerbot_program_position,
setter=set_fingerbot_program_position,
),
],
),
**dict.fromkeys(
Expand Down Expand Up @@ -261,8 +372,8 @@ class TuyaBLECategoryNumberMapping:
products={
**dict.fromkeys(
[
"drlajpqc",
"nhj2j7su",
"drlajpqc",
"nhj2j7su",
], # Thermostatic Radiator Valve
[
TuyaBLENumberMapping(
Expand All @@ -275,7 +386,6 @@ class TuyaBLECategoryNumberMapping:
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
native_step=1,
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=True,
),
),
],
Expand Down Expand Up @@ -308,7 +418,7 @@ class TuyaBLECategoryNumberMapping:
dp_id=103,
description=NumberEntityDescription(
key="recommended_water_intake",
device_class= NumberDeviceClass.WATER,
device_class=NumberDeviceClass.WATER,
native_max_value=5000,
native_min_value=0,
native_unit_of_measurement=VOLUME_MILLILITERS,
Expand Down Expand Up @@ -366,8 +476,8 @@ def native_value(self) -> float | None:
def set_native_value(self, value: float) -> None:
"""Set new value."""
if self._mapping.setter:
if self._mapping.setter(self, self._product, value):
return
self._mapping.setter(self, self._product, value)
return
int_value = int(value * self._mapping.coefficient)
datapoint = self._device.datapoints.get_or_create(
self._mapping.dp_id,
Expand All @@ -386,22 +496,6 @@ def available(self) -> bool:
return result


class TuyaBLEFingerbotPosition(TuyaBLENumber, RestoreEntity):
def __init__(
self,
hass: HomeAssistant,
coordinator: DataUpdateCoordinator,
device: TuyaBLEDevice,
mapping: TuyaBLENumberMapping,
) -> None:
super().__init__(hass, coordinator, device, mapping)

async def async_internal_added_to_hass(self) -> None:
"""Register this entity as a restorable entity."""
last_state = await self.async_get_last_state()
self._attr_native_value = last_state.state


async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
Expand Down
27 changes: 27 additions & 0 deletions custom_components/tuya_ble/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,33 @@ class TuyaBLECategorySelectMapping:
],
},
),
"znhsb": TuyaBLECategorySelectMapping(
products={
"cdlandip": # Smart water bottle
[
TuyaBLESelectMapping(
dp_id=106,
description=TemperatureUnitDescription(
options=[
UnitOfTemperature.CELSIUS,
UnitOfTemperature.FAHRENHEIT,
],
)
),
TuyaBLESelectMapping(
dp_id=107,
description=SelectEntityDescription(
key="reminder_mode",
options=[
"interval_reminder",
"alarm_reminder",
],
entity_category=EntityCategory.CONFIG,
),
),
],
},
),
}


Expand Down
Loading

0 comments on commit c11a1c5

Please sign in to comment.