Skip to content

Commit 4f27058

Browse files
authored
Add fault binary sensors to tuya dehumidifer (home-assistant#148485)
1 parent 058e1ed commit 4f27058

File tree

7 files changed

+264
-7
lines changed

7 files changed

+264
-7
lines changed

homeassistant/components/tuya/binary_sensor.py

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
from homeassistant.core import HomeAssistant, callback
1616
from homeassistant.helpers.dispatcher import async_dispatcher_connect
1717
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
18+
from homeassistant.util.json import json_loads
1819

1920
from . import TuyaConfigEntry
20-
from .const import TUYA_DISCOVERY_NEW, DPCode
21+
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
2122
from .entity import TuyaEntity
2223

2324

@@ -31,6 +32,9 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription):
3132
# Value or values to consider binary sensor to be "on"
3233
on_value: bool | float | int | str | set[bool | float | int | str] = True
3334

35+
# For DPType.BITMAP, the bitmap_key is used to extract the bit mask
36+
bitmap_key: str | None = None
37+
3438

3539
# Commonly used sensors
3640
TAMPER_BINARY_SENSOR = TuyaBinarySensorEntityDescription(
@@ -71,6 +75,34 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription):
7175
),
7276
TAMPER_BINARY_SENSOR,
7377
),
78+
# Dehumidifier
79+
# https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha
80+
"cs": (
81+
TuyaBinarySensorEntityDescription(
82+
key="tankfull",
83+
dpcode=DPCode.FAULT,
84+
device_class=BinarySensorDeviceClass.PROBLEM,
85+
entity_category=EntityCategory.DIAGNOSTIC,
86+
bitmap_key="tankfull",
87+
translation_key="tankfull",
88+
),
89+
TuyaBinarySensorEntityDescription(
90+
key="defrost",
91+
dpcode=DPCode.FAULT,
92+
device_class=BinarySensorDeviceClass.PROBLEM,
93+
entity_category=EntityCategory.DIAGNOSTIC,
94+
bitmap_key="defrost",
95+
translation_key="defrost",
96+
),
97+
TuyaBinarySensorEntityDescription(
98+
key="wet",
99+
dpcode=DPCode.FAULT,
100+
device_class=BinarySensorDeviceClass.PROBLEM,
101+
entity_category=EntityCategory.DIAGNOSTIC,
102+
bitmap_key="wet",
103+
translation_key="wet",
104+
),
105+
),
74106
# Smart Pet Feeder
75107
# https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld
76108
"cwwsq": (
@@ -343,6 +375,22 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription):
343375
}
344376

345377

378+
def _get_bitmap_bit_mask(
379+
device: CustomerDevice, dpcode: str, bitmap_key: str | None
380+
) -> int | None:
381+
"""Get the bit mask for a given bitmap description."""
382+
if (
383+
bitmap_key is None
384+
or (status_range := device.status_range.get(dpcode)) is None
385+
or status_range.type != DPType.BITMAP
386+
or not isinstance(bitmap_values := json_loads(status_range.values), dict)
387+
or not isinstance(bitmap_labels := bitmap_values.get("label"), list)
388+
or bitmap_key not in bitmap_labels
389+
):
390+
return None
391+
return bitmap_labels.index(bitmap_key)
392+
393+
346394
async def async_setup_entry(
347395
hass: HomeAssistant,
348396
entry: TuyaConfigEntry,
@@ -361,12 +409,23 @@ def async_discover_device(device_ids: list[str]) -> None:
361409
for description in descriptions:
362410
dpcode = description.dpcode or description.key
363411
if dpcode in device.status:
364-
entities.append(
365-
TuyaBinarySensorEntity(
366-
device, hass_data.manager, description
367-
)
412+
mask = _get_bitmap_bit_mask(
413+
device, dpcode, description.bitmap_key
368414
)
369415

416+
if (
417+
description.bitmap_key is None # Regular binary sensor
418+
or mask is not None # Bitmap sensor with valid mask
419+
):
420+
entities.append(
421+
TuyaBinarySensorEntity(
422+
device,
423+
hass_data.manager,
424+
description,
425+
mask,
426+
)
427+
)
428+
370429
async_add_entities(entities)
371430

372431
async_discover_device([*hass_data.manager.device_map])
@@ -386,11 +445,13 @@ def __init__(
386445
device: CustomerDevice,
387446
device_manager: Manager,
388447
description: TuyaBinarySensorEntityDescription,
448+
bit_mask: int | None = None,
389449
) -> None:
390450
"""Init Tuya binary sensor."""
391451
super().__init__(device, device_manager)
392452
self.entity_description = description
393453
self._attr_unique_id = f"{super().unique_id}{description.key}"
454+
self._bit_mask = bit_mask
394455

395456
@property
396457
def is_on(self) -> bool:
@@ -399,6 +460,10 @@ def is_on(self) -> bool:
399460
if dpcode not in self.device.status:
400461
return False
401462

463+
if self._bit_mask is not None:
464+
# For bitmap sensors, check the specific bit mask
465+
return (self.device.status[dpcode] & (1 << self._bit_mask)) != 0
466+
402467
if isinstance(self.entity_description.on_value, set):
403468
return self.device.status[dpcode] in self.entity_description.on_value
404469

homeassistant/components/tuya/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class WorkMode(StrEnum):
8282
class DPType(StrEnum):
8383
"""Data point types."""
8484

85+
BITMAP = "Bitmap"
8586
BOOLEAN = "Boolean"
8687
ENUM = "Enum"
8788
INTEGER = "Integer"

homeassistant/components/tuya/entity.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
from .models import EnumTypeData, IntegerTypeData
1515

1616
_DPTYPE_MAPPING: dict[str, DPType] = {
17-
"Bitmap": DPType.RAW,
18-
"bitmap": DPType.RAW,
17+
"bitmap": DPType.BITMAP,
1918
"bool": DPType.BOOLEAN,
2019
"enum": DPType.ENUM,
2120
"json": DPType.JSON,

homeassistant/components/tuya/strings.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@
5656
},
5757
"tilt": {
5858
"name": "Tilt"
59+
},
60+
"tankfull": {
61+
"name": "Tank full"
62+
},
63+
"defrost": {
64+
"name": "Defrost"
65+
},
66+
"wet": {
67+
"name": "Wet"
5968
}
6069
},
6170
"button": {

tests/components/tuya/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
Platform.LIGHT,
2020
],
2121
"cs_arete_two_12l_dehumidifier_air_purifier": [
22+
Platform.BINARY_SENSOR,
2223
Platform.FAN,
2324
Platform.HUMIDIFIER,
2425
Platform.SELECT,

tests/components/tuya/snapshots/test_binary_sensor.ambr

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,151 @@
11
# serializer version: 1
2+
# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_defrost-entry]
3+
EntityRegistryEntrySnapshot({
4+
'aliases': set({
5+
}),
6+
'area_id': None,
7+
'capabilities': None,
8+
'config_entry_id': <ANY>,
9+
'config_subentry_id': <ANY>,
10+
'device_class': None,
11+
'device_id': <ANY>,
12+
'disabled_by': None,
13+
'domain': 'binary_sensor',
14+
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
15+
'entity_id': 'binary_sensor.dehumidifier_defrost',
16+
'has_entity_name': True,
17+
'hidden_by': None,
18+
'icon': None,
19+
'id': <ANY>,
20+
'labels': set({
21+
}),
22+
'name': None,
23+
'options': dict({
24+
}),
25+
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
26+
'original_icon': None,
27+
'original_name': 'Defrost',
28+
'platform': 'tuya',
29+
'previous_unique_id': None,
30+
'suggested_object_id': None,
31+
'supported_features': 0,
32+
'translation_key': 'defrost',
33+
'unique_id': 'tuya.bf3fce6af592f12df3gbgqdefrost',
34+
'unit_of_measurement': None,
35+
})
36+
# ---
37+
# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_defrost-state]
38+
StateSnapshot({
39+
'attributes': ReadOnlyDict({
40+
'device_class': 'problem',
41+
'friendly_name': 'Dehumidifier Defrost',
42+
}),
43+
'context': <ANY>,
44+
'entity_id': 'binary_sensor.dehumidifier_defrost',
45+
'last_changed': <ANY>,
46+
'last_reported': <ANY>,
47+
'last_updated': <ANY>,
48+
'state': 'off',
49+
})
50+
# ---
51+
# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_tank_full-entry]
52+
EntityRegistryEntrySnapshot({
53+
'aliases': set({
54+
}),
55+
'area_id': None,
56+
'capabilities': None,
57+
'config_entry_id': <ANY>,
58+
'config_subentry_id': <ANY>,
59+
'device_class': None,
60+
'device_id': <ANY>,
61+
'disabled_by': None,
62+
'domain': 'binary_sensor',
63+
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
64+
'entity_id': 'binary_sensor.dehumidifier_tank_full',
65+
'has_entity_name': True,
66+
'hidden_by': None,
67+
'icon': None,
68+
'id': <ANY>,
69+
'labels': set({
70+
}),
71+
'name': None,
72+
'options': dict({
73+
}),
74+
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
75+
'original_icon': None,
76+
'original_name': 'Tank full',
77+
'platform': 'tuya',
78+
'previous_unique_id': None,
79+
'suggested_object_id': None,
80+
'supported_features': 0,
81+
'translation_key': 'tankfull',
82+
'unique_id': 'tuya.bf3fce6af592f12df3gbgqtankfull',
83+
'unit_of_measurement': None,
84+
})
85+
# ---
86+
# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_tank_full-state]
87+
StateSnapshot({
88+
'attributes': ReadOnlyDict({
89+
'device_class': 'problem',
90+
'friendly_name': 'Dehumidifier Tank full',
91+
}),
92+
'context': <ANY>,
93+
'entity_id': 'binary_sensor.dehumidifier_tank_full',
94+
'last_changed': <ANY>,
95+
'last_reported': <ANY>,
96+
'last_updated': <ANY>,
97+
'state': 'off',
98+
})
99+
# ---
100+
# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_wet-entry]
101+
EntityRegistryEntrySnapshot({
102+
'aliases': set({
103+
}),
104+
'area_id': None,
105+
'capabilities': None,
106+
'config_entry_id': <ANY>,
107+
'config_subentry_id': <ANY>,
108+
'device_class': None,
109+
'device_id': <ANY>,
110+
'disabled_by': None,
111+
'domain': 'binary_sensor',
112+
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
113+
'entity_id': 'binary_sensor.dehumidifier_wet',
114+
'has_entity_name': True,
115+
'hidden_by': None,
116+
'icon': None,
117+
'id': <ANY>,
118+
'labels': set({
119+
}),
120+
'name': None,
121+
'options': dict({
122+
}),
123+
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
124+
'original_icon': None,
125+
'original_name': 'Wet',
126+
'platform': 'tuya',
127+
'previous_unique_id': None,
128+
'suggested_object_id': None,
129+
'supported_features': 0,
130+
'translation_key': 'wet',
131+
'unique_id': 'tuya.bf3fce6af592f12df3gbgqwet',
132+
'unit_of_measurement': None,
133+
})
134+
# ---
135+
# name: test_platform_setup_and_discovery[cs_arete_two_12l_dehumidifier_air_purifier][binary_sensor.dehumidifier_wet-state]
136+
StateSnapshot({
137+
'attributes': ReadOnlyDict({
138+
'device_class': 'problem',
139+
'friendly_name': 'Dehumidifier Wet',
140+
}),
141+
'context': <ANY>,
142+
'entity_id': 'binary_sensor.dehumidifier_wet',
143+
'last_changed': <ANY>,
144+
'last_reported': <ANY>,
145+
'last_updated': <ANY>,
146+
'state': 'off',
147+
})
148+
# ---
2149
# name: test_platform_setup_and_discovery[mcs_door_sensor][binary_sensor.door_garage_door-entry]
3150
EntityRegistryEntrySnapshot({
4151
'aliases': set({

tests/components/tuya/test_binary_sensor.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,38 @@ async def test_platform_setup_no_discovery(
5656
assert not er.async_entries_for_config_entry(
5757
entity_registry, mock_config_entry.entry_id
5858
)
59+
60+
61+
@pytest.mark.parametrize(
62+
"mock_device_code",
63+
["cs_arete_two_12l_dehumidifier_air_purifier"],
64+
)
65+
@pytest.mark.parametrize(
66+
("fault_value", "tankfull", "defrost", "wet"),
67+
[
68+
(0, "off", "off", "off"),
69+
(0x1, "on", "off", "off"),
70+
(0x2, "off", "on", "off"),
71+
(0x80, "off", "off", "on"),
72+
(0x83, "on", "on", "on"),
73+
],
74+
)
75+
@patch("homeassistant.components.tuya.PLATFORMS", [Platform.BINARY_SENSOR])
76+
async def test_bitmap(
77+
hass: HomeAssistant,
78+
mock_manager: ManagerCompat,
79+
mock_config_entry: MockConfigEntry,
80+
mock_device: CustomerDevice,
81+
fault_value: int,
82+
tankfull: str,
83+
defrost: str,
84+
wet: str,
85+
) -> None:
86+
"""Test BITMAP fault sensor on cs_arete_two_12l_dehumidifier_air_purifier."""
87+
mock_device.status["fault"] = fault_value
88+
89+
await initialize_entry(hass, mock_manager, mock_config_entry, mock_device)
90+
91+
assert hass.states.get("binary_sensor.dehumidifier_tank_full").state == tankfull
92+
assert hass.states.get("binary_sensor.dehumidifier_defrost").state == defrost
93+
assert hass.states.get("binary_sensor.dehumidifier_wet").state == wet

0 commit comments

Comments
 (0)