Skip to content

Commit 83c11fb

Browse files
authored
Merge pull request #1444 from custom-components/linp_es3
Add Linptech Human Presence Sensor ES3
2 parents 15d8654 + 2b55b5f commit 83c11fb

File tree

5 files changed

+109
-0
lines changed

5 files changed

+109
-0
lines changed

custom_components/ble_monitor/ble_parser/xiaomi.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
0x3F4C: "PS1BB",
8282
0x3A61: "KS1",
8383
0x3E17: "KS1BP",
84+
0x50FB: "ES3",
8485
0x5DB1: "MBS17"
8586
}
8687

@@ -815,6 +816,34 @@ def obj4840(xobj):
815816
return {"pressure not present time set": duration}
816817

817818

819+
def obj484e(xobj):
820+
"""Occupancy Status"""
821+
(occupancy,) = struct.unpack("<B", xobj)
822+
if occupancy == 0:
823+
# no motion is being taken care of by the timer in HA
824+
return {}
825+
else:
826+
return {"motion": 1, "motion timer": 1}
827+
828+
829+
def obj484f(xobj):
830+
"""Time in minutes of no motion (not used, we use 484e)"""
831+
if len(xobj) == 1:
832+
(no_motion_time,) = struct.unpack("<B", xobj)
833+
# minutes of no motion (not used, we use motion timer in obj4a08)
834+
# 0 = motion detected
835+
return {"motion": 1 if no_motion_time == 0 else 0, "no motion time": no_motion_time}
836+
else:
837+
return {}
838+
839+
840+
def obj4850(xobj):
841+
"""Time in minutes with motion (not used, we use 484e)"""
842+
(motion_time,) = struct.unpack("<B", xobj)
843+
# minutes with motion (not used, we use motion timer in obj484e)
844+
return {"motion time": motion_time}
845+
846+
818847
def obj4a01(xobj):
819848
"""Low Battery"""
820849
low_batt = xobj[0]
@@ -1270,6 +1299,9 @@ def obj5a16(xobj):
12701299
0x483e: obj483e,
12711300
0x483f: obj483f,
12721301
0x4840: obj4840,
1302+
0x484e: obj484e,
1303+
0x484f: obj484f,
1304+
0x4850: obj4850,
12731305
0x4a01: obj4a01,
12741306
0x4a08: obj4a08,
12751307
0x4a0c: obj4a0c,

custom_components/ble_monitor/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2232,6 +2232,7 @@ class BLEMonitorBinarySensorEntityDescription(
22322232
'MMC-W505' : 'Xiaomi',
22332233
'SJWS01LM' : 'Xiaomi',
22342234
'RS1BB' : 'Linptech',
2235+
'ES3' : 'Linptech',
22352236
}
22362237

22372238

custom_components/ble_monitor/test/test_xiaomi_parser.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,59 @@ def test_linptech_HS1BB_illuminance_motion(self):
931931
assert sensor_msg["illuminance"] == 228.0
932932
assert sensor_msg["rssi"] == -58
933933

934+
def test_linptech_ES3_illuminance(self):
935+
"""Test Xiaomi parser for linptech ES3."""
936+
self.aeskeys = {}
937+
data_string = "043E260201000176c3c738c1a41a020106161695fe4859fb50d986d27e8f5313e900000030ad6da8C6"
938+
data = bytes(bytearray.fromhex(data_string))
939+
940+
aeskey = "b26295a7a08fbac306c8706ade7f0fe4"
941+
942+
is_ext_packet = True if data[3] == 0x0D else False
943+
mac = (data[8 if is_ext_packet else 7:14 if is_ext_packet else 13])[::-1]
944+
mac_address = mac.hex()
945+
p_mac = bytes.fromhex(mac_address.replace(":", "").lower())
946+
p_key = bytes.fromhex(aeskey.lower())
947+
self.aeskeys[p_mac] = p_key
948+
# pylint: disable=unused-variable
949+
ble_parser = BleParser(aeskeys=self.aeskeys)
950+
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)
951+
952+
assert sensor_msg["firmware"] == "Xiaomi (MiBeacon V5 encrypted)"
953+
assert sensor_msg["type"] == "ES3"
954+
assert sensor_msg["mac"] == "A4C138C7C376"
955+
assert sensor_msg["packet"] == 217
956+
assert sensor_msg["data"]
957+
assert sensor_msg["illuminance"] == 173.0
958+
assert sensor_msg["rssi"] == -58
959+
960+
def test_linptech_ES3_motion(self):
961+
"""Test Xiaomi parser for linptech ES3."""
962+
self.aeskeys = {}
963+
data_string = "043E290201000176c3c738c1a41D020106191695fe5859fb50da76c3c738c1a4aabc4c16000000c60c1646C6"
964+
data = bytes(bytearray.fromhex(data_string))
965+
966+
aeskey = "b26295a7a08fbac306c8706ade7f0fe4"
967+
968+
is_ext_packet = True if data[3] == 0x0D else False
969+
mac = (data[8 if is_ext_packet else 7:14 if is_ext_packet else 13])[::-1]
970+
mac_address = mac.hex()
971+
p_mac = bytes.fromhex(mac_address.replace(":", "").lower())
972+
p_key = bytes.fromhex(aeskey.lower())
973+
self.aeskeys[p_mac] = p_key
974+
# pylint: disable=unused-variable
975+
ble_parser = BleParser(aeskeys=self.aeskeys)
976+
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)
977+
978+
assert sensor_msg["firmware"] == "Xiaomi (MiBeacon V5 encrypted)"
979+
assert sensor_msg["type"] == "ES3"
980+
assert sensor_msg["mac"] == "A4C138C7C376"
981+
assert sensor_msg["packet"] == 218
982+
assert sensor_msg["data"]
983+
assert sensor_msg["motion"] == 1
984+
assert sensor_msg["motion timer"] == 1
985+
assert sensor_msg["rssi"] == -58
986+
934987
def test_MJZNZ018H_bed_occupancy(self):
935988
"""Test Xiaomi parser for MJZNZ018H bed occupancy sensor."""
936989
self.aeskeys = {}

docs/_devices/Linptech ES3.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
manufacturer: Linptech
3+
name: Linptech Human Presence Sensor
4+
model: ES3
5+
image: Linptech_ES3.png
6+
physical_description:
7+
broadcasted_properties:
8+
- illuminance
9+
- motion
10+
- battery
11+
- rssi
12+
broadcasted_property_notes:
13+
- property: illuminance
14+
note: is measured in lux.
15+
- property: motion
16+
note: Motion state is ‘motion detected’ or ‘clear’.
17+
broadcast_rate: See Notes
18+
active_scan:
19+
encryption_key: Yes
20+
custom_firmware:
21+
notes: >
22+
- You can use the [reset_timer](configuration_params#reset_timer) option if you want to use a different time to set the sensor to `motion clear`.
23+
---

docs/assets/images/Linptech_ES3.png

16.5 KB
Loading

0 commit comments

Comments
 (0)