Skip to content

Commit

Permalink
Merge pull request #1247 from custom-components/teltonika_eye
Browse files Browse the repository at this point in the history
Add Teltonika EYE sensor
  • Loading branch information
Ernst79 authored Oct 8, 2023
2 parents 3787d42 + 8caa137 commit 48abe3b
Show file tree
Hide file tree
Showing 12 changed files with 198 additions and 6 deletions.
11 changes: 11 additions & 0 deletions custom_components/ble_monitor/ble_parser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,13 @@ def parse_advertisement(
# Jaalee
sensor_data = parse_jaalee(self, man_spec_data, mac, rssi)
break
elif len(man_spec_data_list) == 2:
# Teltonika Eye (can send both iBeacon and Teltonika data in one message)
second_man_spec_data = man_spec_data_list[1]
second_comp_id = (second_man_spec_data[3] << 8) | second_man_spec_data[2]
if second_comp_id == 0x089A:
sensor_data = parse_teltonika(self, second_man_spec_data, local_name, mac, rssi)
break
else:
# iBeacon
sensor_data, tracker_data = parse_ibeacon(self, man_spec_data, mac, rssi)
Expand Down Expand Up @@ -325,6 +332,10 @@ def parse_advertisement(
man_spec_data = b"".join(man_spec_data_list)
sensor_data = parse_hormann(self, man_spec_data, mac, rssi)
break
elif comp_id == 0x089A:
# Teltonika
sensor_data = parse_teltonika(self, man_spec_data, local_name, mac, rssi)
break
elif comp_id == 0x094F and data_len == 0x15:
# Mikrotik
sensor_data = parse_mikrotik(self, man_spec_data, mac, rssi)
Expand Down
2 changes: 1 addition & 1 deletion custom_components/ble_monitor/ble_parser/bthome_const.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class MeasTypes:
factor=0.01,
),
0x15: MeasTypes(
meas_format="battery",
meas_format="battery low",
),
0x16: MeasTypes(
meas_format="battery charging",
Expand Down
48 changes: 47 additions & 1 deletion custom_components/ble_monitor/ble_parser/teltonika.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ def parse_teltonika(self, data, complete_local_name, source_mac, rssi):
"""Teltonika parser"""
result = {"firmware": "Teltonika"}
teltonika_mac = source_mac
device_id = (data[3] << 8) | data[2]

if complete_local_name == "PUCK_T1":
if device_id == 0x089A:
device_type = "EYE sensor"
elif complete_local_name == "PUCK_T1":
device_type = "Blue Puck T"
elif complete_local_name == "PUCK_TH":
device_type = "Blue Puck RHT"
Expand Down Expand Up @@ -62,6 +65,49 @@ def parse_teltonika(self, data, complete_local_name, source_mac, rssi):
# Battery
(batt,) = unpack("<b", packet[5:6])
result.update({"battery": batt})
elif comp_id == 0x089A:
flags = packet[5]
sensor_data = packet[6:]
if flags & (1 << 0): # bit 0
# Temperature
(temp,) = unpack(">h", sensor_data[0:2])
result.update({"temperature": temp / 100})
sensor_data = sensor_data[2:]
if flags & (1 << 1): # bit 1
# Humidity
humi = sensor_data[0]
result.update({"humidity": humi})
sensor_data = sensor_data[1:]
if flags & (1 << 2): # bit 2
# Magnetic sensor presence
if flags & (1 << 3): # bit 3
# magnetic field is detected
result.update({"magnetic field detected": 1})
else:
# magnetic field is not detected
result.update({"magnetic field detected": 0})
if flags & (1 << 4): # bit 4
# Movement sensor counter
# Most significant bit indicates movement state
# 15 least significant bits represent count of movement events.
moving = sensor_data[0] & (1 << 7)
count = ((sensor_data[0] & 0b01111111) << 8) + sensor_data[1]
result.update({"moving": moving, "movement counter": count})
sensor_data = sensor_data[2:]
if flags & (1 << 5): # bit 5
# Movement sensor angle
# Most significant byte – pitch (-90/+90)
# Two least significant bytes – roll (-180/+180)
(pitch, roll,) = unpack(">bh", sensor_data[0:3])
result.update({"roll": roll, "pitch": pitch})
sensor_data = sensor_data[3:]
if flags & (1 << 6): # bit 6
# Low battery indication sensor presence
result.update({"battery low": 1})
if flags & (1 << 7): # bit 7
# Battery voltage value presence
volt = round(2.0 + sensor_data[0] * 0.01, 3)
result.update({"voltage": volt})
data_size -= packet_size
packet_start += packet_size

Expand Down
69 changes: 65 additions & 4 deletions custom_components/ble_monitor/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,15 @@ class BLEMonitorBinarySensorEntityDescription(
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
force_update=True,
),
BLEMonitorBinarySensorEntityDescription(
key="battery low",
sensor_class="BaseBinarySensor",
update_behavior="Instantly",
name="battery low",
unique_id="bl_",
device_class=BinarySensorDeviceClass.BATTERY,
force_update=True,
),
BLEMonitorBinarySensorEntityDescription(
key="carbon monoxide",
sensor_class="BaseBinarySensor",
Expand Down Expand Up @@ -465,6 +474,16 @@ class BLEMonitorBinarySensorEntityDescription(
device_class=None,
force_update=False,
),
BLEMonitorBinarySensorEntityDescription(
key="magnetic field detected",
sensor_class="BaseBinarySensor",
update_behavior="Instantly",
name="magnetic field",
unique_id="magnetic_field_",
icon="mdi:magnet",
device_class=None,
force_update=False,
),
BLEMonitorBinarySensorEntityDescription(
key="bed occupancy",
sensor_class="BaseBinarySensor",
Expand Down Expand Up @@ -1035,6 +1054,30 @@ class BLEMonitorBinarySensorEntityDescription(
suggested_display_precision=1,
state_class=SensorStateClass.MEASUREMENT,
),
BLEMonitorSensorEntityDescription(
key="roll",
sensor_class="InstantUpdateSensor",
update_behavior="Instantly",
name="roll",
unique_id="roll_",
icon="mdi:horizontal-rotate-clockwise",
native_unit_of_measurement="°",
device_class=None,
suggested_display_precision=0,
state_class=SensorStateClass.MEASUREMENT,
),
BLEMonitorSensorEntityDescription(
key="pitch",
sensor_class="InstantUpdateSensor",
update_behavior="Instantly",
name="pitch",
unique_id="pitch_",
icon="mdi:rotate-right-variant",
native_unit_of_measurement="°",
device_class=None,
suggested_display_precision=0,
state_class=SensorStateClass.MEASUREMENT,
),
BLEMonitorSensorEntityDescription(
key="distance",
sensor_class="InstantUpdateSensor",
Expand Down Expand Up @@ -1226,6 +1269,18 @@ class BLEMonitorBinarySensorEntityDescription(
suggested_display_precision=0,
state_class=SensorStateClass.MEASUREMENT,
),
BLEMonitorSensorEntityDescription(
key="movement counter",
sensor_class="StateChangedSensor",
update_behavior="StateChange",
name="movement counter",
unique_id="mv_cnt_",
icon="mdi:counter",
native_unit_of_measurement=None,
device_class=None,
suggested_display_precision=0,
state_class=SensorStateClass.MEASUREMENT,
),
BLEMonitorSensorEntityDescription(
key="score",
sensor_class="StateChangedSensor",
Expand Down Expand Up @@ -1796,18 +1851,23 @@ class BLEMonitorBinarySensorEntityDescription(
# Sensors that support automatic adding of sensors and binary sensors
AUTO_MANUFACTURER_DICT = {
'Amazfit Smart Scale' : 'Amazfit',
'Blustream' : 'Blustream',
'BTHome' : 'BTHome',
'HHCCJCY10' : 'HHCC',
'HolyIOT BLE tracker' : 'HolyIOT',
'Supramatic E4 BS' : 'Hörmann',
'IBS-TH' : 'Inkbird',
'IBS-TH2/P01B' : 'Inkbird',
'JHT' : 'Jaalee',
'TG-BT5-IN' : 'Mikrotik',
'TG-BT5-OUT' : 'Mikrotik',
'Electra Washbasin Faucet': 'Oras',
'EClerk Eco' : 'Relsib',
'WT51' : 'Relsib',
'Blue Puck T' : 'Teltonika',
'Blue Coin T' : 'Teltonika',
'Blue Puck RHT' : 'Teltonika',
'EYE sensor' : 'Teltonika',
'TP357' : 'Thermopro',
'TP359' : 'Thermopro',
'Tilt Red' : 'Tilt',
Expand All @@ -1819,16 +1879,13 @@ class BLEMonitorBinarySensorEntityDescription(
'Tilt Yellow' : 'Tilt',
'Tilt Pink' : 'Tilt',
'MMC-W505' : 'Xiaomi',
'Electra Washbasin Faucet': 'Oras',
'Supramatic E4 BS' : 'Hörmann',
'Blustream' : 'Blustream',
'HolyIOT BLE tracker' : 'HolyIOT',
}


# Binary Sensors that are automatically added if device is in AUTO_MANUFACTURER_DICT
AUTO_BINARY_SENSOR_LIST = [
"battery charging",
"battery low",
"carbon monoxide",
"cold",
"connectivity",
Expand All @@ -1841,6 +1898,7 @@ class BLEMonitorBinarySensorEntityDescription(
"impact",
"light",
"lock",
"magnetic field detected",
"motion",
"moisture detected",
"moving",
Expand Down Expand Up @@ -1884,13 +1942,16 @@ class BLEMonitorBinarySensorEntityDescription(
"illuminance",
"impedance",
"moisture",
"movement counter",
"non-stabilized weight",
"opening percentage",
"pitch",
"pm2.5",
"pm10",
"power",
"pressure",
"pulse",
"roll",
"rotation",
"speed",
"steps",
Expand Down
3 changes: 3 additions & 0 deletions custom_components/ble_monitor/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,8 @@ class BaseSensor(RestoreSensor, SensorEntity):
# | |**pulse
# | |**shake
# | |**rotation
# | |**roll
# | |**pitch
# | |**distance
# | |**distance mm
# | |**duration
Expand All @@ -354,6 +356,7 @@ class BaseSensor(RestoreSensor, SensorEntity):
# | | |**major
# | | |**minor
# | | |**count
# | | |**movement counter
# | | |**score
# | | |**air quality
# | | |**text
Expand Down
17 changes: 17 additions & 0 deletions custom_components/ble_monitor/test/test_bthome_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,23 @@ def test_bthome_v2_moisture(self):
assert sensor_msg["moisture"] == 3.07
assert sensor_msg["rssi"] == -52

def test_bthome_v2_battery_low(self):
"""Test BTHome parser for battery low measurement"""
data_string = "043E1602010000A5808FE648540A0201060616D2FC401501CC"
data = bytes(bytearray.fromhex(data_string))

# pylint: disable=unused-variable
ble_parser = BleParser()
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)

assert sensor_msg["firmware"] == "BTHome V2"
assert sensor_msg["type"] == "BTHome"
assert sensor_msg["mac"] == "5448E68F80A5"
assert sensor_msg["packet"] == "no packet id"
assert sensor_msg["data"]
assert sensor_msg["battery low"] == 1
assert sensor_msg["rssi"] == -52

def test_bthome_v2_battery_charging(self):
"""Test BTHome parser for battery charging measurement"""
data_string = "043E1602010000A5808FE648540A0201060616D2FC401601CC"
Expand Down
23 changes: 23 additions & 0 deletions custom_components/ble_monitor/test/test_teltonika.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,26 @@ def test_ela_blue_puck_T_with_batt(self):
assert sensor_msg["temperature"] == 27.12
assert sensor_msg["battery"] == 13
assert sensor_msg["rssi"] == -35

def test_teltonika_eye(self):
"""Test Teltonika parser for Teltonika Eye sensor."""
data_string = "043E46020103001897035ECFD03a0201061AFF4C000215FFFFFFFF0B8C404510C655AAB636EBEFBB700055020C094D50315F313233343536370EFF9A0801B708B4120CCB0BFFC767DD"
data = bytes(bytearray.fromhex(data_string))
# pylint: disable=unused-variable
ble_parser = BleParser()
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)

assert sensor_msg["firmware"] == "Teltonika"
assert sensor_msg["type"] == "EYE sensor"
assert sensor_msg["mac"] == "D0CF5E039718"
assert sensor_msg["packet"] == "no packet id"
assert sensor_msg["data"]
assert sensor_msg["temperature"] == 22.28
assert sensor_msg["humidity"] == 18
assert sensor_msg["magnetic field detected"] == 0
assert sensor_msg["moving"] == 0
assert sensor_msg["movement counter"] == 3275
assert sensor_msg["roll"] == -57
assert sensor_msg["pitch"] == 11
assert sensor_msg["voltage"] == 3.03
assert sensor_msg["rssi"] == -35
File renamed without changes.
File renamed without changes.
File renamed without changes.
31 changes: 31 additions & 0 deletions docs/_devices/Teltonika_eye_beacon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
manufacturer: Teltonika
name: Eye Beacon
model: Eye Beacon
image: Teltonika_eye.png
physical_description: Rounded beacon, no screen
broadcasted_properties:
- temperature
- humidity
- roll
- pitch
- magnetic field detected
- moving
- movement counter
- battery low
- rssi
broadcasted_property_notes:
broadcast_rate:
active_scan: see notes
encryption_key:
custom_firmware:
notes:
- Teltonika EYE sensors can send BLE data in 3 formats
1. iBeacon + EYE Sensors
2. Eddystone + EYE Sensors
3. EYE Sensors

For iBeacon + EYE Sensors and Eddystone + EYE Sensors protocols only iBeacon/Eddystone packet is broadcasted and will be seen by both active and passive scans, to see the EYE Sensors packet you need to use active scan. In other words in an environment where no BLE devices are scanning with an active scan or in case when there are no scanning devices at all, only the iBeacon/Eddystone packet will be sent by the BTS device to conserve energy.

This means that if you use the iBeacon (1) or Eddystone format (2), you will need to enable active scan. If you are only using EYE sensors format (3), you can use passive scanning in BLE monitor.
---
Binary file added docs/assets/images/Teltonika_eye.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 48abe3b

Please sign in to comment.